package com.limegroup.gnutella.downloader; import java.util.Random; import java.util.Iterator; import java.util.NoSuchElementException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.util.IntervalSet; /** * This SelectionStrategy selects random Intervals from the availableIntervals. * * If the number of Intervals contained in neededBytes is less than MAX_FRAGMENTS, * then a random location between the first and last bytes (inclusive) of neededBytes * is chosen. We find the last chunk before this location and the first chunk after. * We return one or the other chunk (as an Interval) with equal probability. Of * course, if there are only available bytes on one side of the location, then there * is only one choice for which chunk to return. For network efficiency, the random * location is aligned to blockSize boundaries. * * If the number of Intervals in neededBytes is greater than or equal to MAX_FRAGMENTS, * then the same algorithm is used, except that the location is chosen randomly from * an endpoint of one of the existing fragments, in an attempt to coalesce fragments. * */ public class RandomDownloadStrategy implements SelectionStrategy { private static final Log LOG = LogFactory.getLog(RandomDownloadStrategy.class); /** Maximum number of file framgents we're willing to intentionally create */ private static final int MAX_FRAGMENTS = 16; /** * A global pseudorandom number generator. We don't really care about values * duplicated across threads, so don't bother serializing access. * * This really should be final, except that making it non-final makes tests * much more simple. */ protected static Random pseudoRandom = new Random(); /** The size the download will be once completed. */ protected final long completedSize; public RandomDownloadStrategy(long completedSize) { super(); this.completedSize = completedSize; } /** * Picks a random block of a file to download next. * * For efficiency reasons attempts will be made to align the start and end of * intervals to block boundaries. However, there are no guarantees on alignment. * * @param candidateBytes a representation of the set of * bytes available for download from a given server, minus the set * of bytes that have already been leased, verified, etc. * This guarantees candidateBytes is a subset of neededBytes. * @param neededBytes a representation of the set of bytes * of the file that have not been leased, verified, etc. * @param fileSize the total length of the file being downloaded * @param blockSize the maximum size of the returned Interval. Any values less than 1 will * be ignared. An attempt will be made to make the high end of the interval one less * than a multiple of blockSize. Any values less than 1 will generate IllegalArgumentExceptions. * @return the Interval that should be assigned next, with a size of at most blockSize bytes * @throws NoSuchElementException if passed an empty IntervalSet */ public Interval pickAssignment(IntervalSet candidateBytes, IntervalSet neededBytes, long blockSize) throws java.util.NoSuchElementException { long lowerBound = neededBytes.getFirst().low; long upperBound = neededBytes.getLast().high; if (blockSize < 1) throw new IllegalArgumentException("Block size cannot be "+blockSize); if (lowerBound < 0) throw new IllegalArgumentException("lowerBound must be >= 0, "+lowerBound+"<0"); if (upperBound >= completedSize) throw new IllegalArgumentException("Greatest needed byte must be less than completedSize "+ upperBound+" >= "+completedSize); if (candidateBytes.isEmpty()) throw new NoSuchElementException(); // The returned Interval will be the last chunk before idealLocation // or the first chunk after idealLocation long idealLocation = getIdealLocation(neededBytes, blockSize); // The first properly aligned interval, returned in the case that // there are no aligned intervals available after lowerBound Interval lastSuitableInterval = null; Iterator intervalIterator = candidateBytes.getAllIntervals(); // First aligned chunk after idealLocation Interval intervalAbove = null; // Last aligned chunk before idealLocation Interval intervalBelow = null; while (intervalIterator.hasNext()) { Interval candidateInterval = (Interval) intervalIterator.next(); if (candidateInterval.low < idealLocation) intervalBelow = optimizeIntervalBelow(candidateInterval, idealLocation, blockSize); if (candidateInterval.high >= idealLocation) { intervalAbove = optimizeIntervalAbove(candidateInterval,idealLocation, blockSize); // Since we started iterating from the low end of candidateBytes, // the first intervalAbove is the one closest to idealLocation // and there will be no more changes in intervalBelow break; } } if (LOG.isDebugEnabled()) LOG.debug("idealLocation="+idealLocation +" intervalAbove="+intervalAbove +" intervalBelow="+intervalBelow +" out of possibilites:"+candidateBytes); // If candidateBytes is not empty, at least one of // intervalAbove or intervalBelow is not null. // If we don't have a choice, return the Interval that makes sense if (intervalAbove == null) return intervalBelow; if (intervalBelow == null) return intervalAbove; // We have a choice, so return each with equal probability. return ((pseudoRandom.nextInt()&1) == 1) ? intervalAbove : intervalBelow; } ///////////////////// Private Helper Methods ///////////////////////////////// /** Aligns location to one byte before the next highest block boundary */ protected long alignHigh(long location, long blockSize) { location += blockSize; location -= location % blockSize; return location - 1; } /** Aligns location to the nearest block boundary that is at or before location */ protected long alignLow(long location, long blockSize) { location -= location % blockSize; return location; } /** * Calculates the "ideal location" on which to base an assignment. */ private long getIdealLocation(IntervalSet neededBytes, long blockSize) { int fragmentCount = neededBytes.getNumberOfIntervals(); if (fragmentCount >= MAX_FRAGMENTS) { // No fragments to spare, so attempt to reduce fragmentation by // setting idealLocation to the first byte of any fragment, or // the last byte of the last fragment. // Since we download on either side of the idealLocation, this has // the effect of "growing" our contiguous blocks of downloaded data // in both directions until they coalesce. int randomFragmentNumber = pseudoRandom.nextInt(fragmentCount + 1); if (randomFragmentNumber == fragmentCount) return neededBytes.getLast().high + 1; else return ((Interval)neededBytes.getAllIntervalsAsList().get(randomFragmentNumber)).low; } else { // There are fragments to spare, so download from a random location return getRandomLocation(neededBytes.getFirst().low, neededBytes.getLast().high, blockSize); } } /** Returns candidate or a sub-interval of candidate that best * fits the following goals: * * 1) returnInterval.low >= location * 2) returnInterval.low is as close to location as possible * 3) returnInterval does not span a blockSize boundary * 4) returnInterval is as large as possible without violating goals 1-3 * * Required precondition: candidate.high >= location */ private Interval optimizeIntervalAbove(Interval candidate, long location, long blockSize) { // Calculate the most suitable low value contained // in candidate. (satisfying goals 1 & 2) long bestLow = candidate.low; if (bestLow < location) { bestLow = location; } // Calculate the most suitable high byte based on goal 3 // This will be at most blockSize-1 bytes greater than bestLow long bestHigh = alignHigh(bestLow,blockSize); if (bestHigh > candidate.high) bestHigh = candidate.high; if (candidate.high == bestHigh && candidate.low == bestLow) return candidate; return new Interval(bestLow,bestHigh); } /** Returns candidate or a sub-interval of candidate that best * fits the following goals: * * 1) returnInterval.high <= location * 2) returnInterval.high is as close to location as possible * 3) returnInterval does not span a blockSize boundary * 4) returnInterval is as large as possible without violating goals 1-3 * * Required precondition: candidate.low < location */ private Interval optimizeIntervalBelow(Interval candidate, long location, long blockSize) { // Calculate the most suitable low value contained // in candidate. (satisfying goals 1 & 2) long bestHigh = candidate.high; if (bestHigh >= location) { bestHigh = location - 1; } // Calculate the most suitable high byte based on goal 3 // This will be at most blockSize-1 bytes greater than bestLow long bestLow = alignLow(bestHigh,blockSize); if (bestLow < candidate.low) bestLow = candidate.low; if (candidate.high == bestHigh && candidate.low == bestLow) return candidate; return new Interval(bestLow,bestHigh); } /** * Calculates a random block-aligned byte offset into the file, * at least minIndex bytes into the file. If minIndex is less than blockSize * from maxIndex, minIndex will be returned, regardless of its alignment. * * This function is safe for files larger than 2 GB, files with chunks larger than 2 GB, * and files containing more than 2 GiBi chunks. * * This function is practically unbiased for files smaller than several terabytes. */ private long getRandomLocation(long minIndex, long maxIndex, long blockSize) { // If minIndex is in the middle of a block, include the // beginning of that block. long minBlock = minIndex / blockSize; // If maxIndex is in the middle of a block, include that // partial block in our range long maxBlock = maxIndex / blockSize; // This may happen if there is only one block available to be assigned. // ... just give back the minIndex if (minBlock >= maxBlock) return minIndex; //No need to align the last partial block // Generate a random blockNumber on the range [minBlock, maxBlock] // return blockSize * blockNumber return blockSize * (minBlock + Math.abs(pseudoRandom.nextLong() % (maxBlock-minBlock+1))); } }