package org.dcache.poolmanager;
import com.google.common.base.Function;
import java.io.Serializable;
import java.security.SecureRandom;
import java.util.List;
import diskCacheV111.pools.PoolCostInfo;
import static java.util.concurrent.TimeUnit.DAYS;
/**
* Pool selection algorithm using Weighted Available Space Selection (WASS).
*
* The weighted available space is defined as:
*
* scf
* available
* ----------------------
* (pcf writers)
* 2
*
* where available is the unweighted available space, writers the
* current number of write movers, pcf is the performance cost
* factor, and scf is the space cost factor.
*
* The space cost factor adjusts the preference for using pools by
* available space. A space cost factor of 0 means that the
* selection is independent of available space. A value of 1 means
* that the preference of a pool is proportional to the amount of
* available space. The higher the value the more the selection is
* skewed to pools with lots of free space (negative values mean
* the selection is more skewed towards pools with little free
* space; that's unlikely to be useful).
*
* A selection purely guided by space risks accumulating writers
* on a pool, eventually causing pools to become overloaded. To
* add a feedback from write activity we reduce the available
* space exponentially with the number of writers.
*
* Intuitively the reciprocal of pcf is the number of writers it
* takes to half the weighted available space.
*
* Setting pcf to 0 means the available space will be unweighted, ie
* load does not affect pool selection. A value of 1 would mean
* that every write half the available space. The useful range of
* pcg is probably 0 to 1.
*
* The performance cost factor used in the expression is the
* product of a per pool value and the performance cost factor of
* the partition. A per pool value makes it possible to specify
* how quickly a pool degrades with load.
*
* Note that setting both factors to zero causes pool selection to
* become random. This it the same behaviour as with the classic
* partition.
*/
public class WeightedAvailableSpaceSelection implements Serializable
{
private static final long serialVersionUID = 6196398425106858164L;
/* SecureRandom is a higher quality source for randomness than
* Random.
*/
protected static final SecureRandom RANDOM = new SecureRandom();
static final double SECONDS_IN_WEEK = DAYS.toSeconds(7);
static final double LOG2 = Math.log(2);
private final double performanceCostFactor;
private final double spaceCostFactor;
public WeightedAvailableSpaceSelection(double performanceCostFactor, double spaceCostFactor)
{
this.performanceCostFactor = performanceCostFactor;
this.spaceCostFactor = spaceCostFactor;
}
protected double random()
{
return RANDOM.nextDouble();
}
/**
* Returns the amount of removable space considered available for writes.
* <p/>
* We treat removable space as decaying at an exponential rate. Ie the longer a removable file
* has not been accessed, the less space we consider it to occupy.
*/
protected double getAvailableRemovable(PoolCostInfo.PoolSpaceInfo space)
{
double removable = space.getRemovableSpace();
double breakeven = space.getBreakEven();
double lru = space.getLRUSeconds();
/* Breakeven is traditionally defined as the classic space
* cost after one week, ie when lru equals one week.
*
* We interpret this as a halflife for removable space such
* that breakeven specifies the undecayed fraction of the
* least recently accessed removable byte.
*
* There is an even older interpretation of breakeven that
* applies when it is 1.0 or larger. Pool manager used it as a
* corrective factor for space cost computation. That is not
* translatable to a halflife and hence we use a constant
* instead.
*
* See also diskCacheV111.pools.CostCalculationV5.
*/
double halflife;
if (breakeven >= 1.0) {
halflife = SECONDS_IN_WEEK * 2;
} else if (breakeven > 0.0) {
halflife = SECONDS_IN_WEEK * -LOG2 / Math.log(breakeven);
} else {
/* Breakeven of zero means that we don't want to take the
* age of removable space into account. Hence we just
* consider it available.
*/
return removable;
}
/* The exponential decay process is defined as
*
* N(t) = n * 0.5 ^ (t / T)
*
* where T is the halflife and n is the size of the removable
* file. Ie. at age t only N(t) of a removable file "still
* exists" (figurably).
*
* Ideally we would know the last access time of each
* removable file on the pool. We do however only know the
* number of removable bytes, r, and the last access time, l,
* of the least recently used removable file.
*
* We linearly interpolate this data such that the age of the
* youngest removable byte is zero and the age of the oldest
* removable byte is lru. That is
*
* age(x) = (l / r) * x
*
* for x being the index of a removable byte.
*
* Combining the above two expressions gives us:
*
* N(x) = 0.5 ^ (age(x) / T) = 0.5 ^ ((l * x) / (r * T))
*
* Here N(x) is the fraction of the x'th byte that isn't
* decayed yet. Note that the file size, n, is no longer in
* the expression because we interpolated the age for each
* byte, not for each file.
*
* We now want the definite integral of N from 0 to r. First,
* the indefinite integral of N is:
*
* r T
* -(---------------------)
* (l x)/(r T)
* 2 l Log[2]
*
* The definite integral from 0 to r then becomes
*
* r * T * (1 - 2 ^ (-l/T)) / (l * Log(2))
*
*/
double undecayed;
if (lru > 0) {
undecayed =
removable * halflife * (1 - Math.pow(2.0, -lru / halflife)) /
(lru * LOG2);
} else {
undecayed = removable;
}
return removable - undecayed;
}
/**
* Returns the available space of a pool.
* <p/>
* Available space includes free space and removable space deemed available for writes. The gap
* parameter of the pool is respected.
*/
protected double getAvailable(PoolCostInfo.PoolSpaceInfo space, long filesize)
{
long free = space.getFreeSpace();
long gap = space.getGap();
/* If the pool cannot hold the file without eating into the gap, the
* pool is considered full.
*/
if (free + space.getRemovableSpace() - filesize <= gap) {
return 0;
}
double removable = getAvailableRemovable(space);
/* The amount of available space on a pool is the sum of
* whatever is free and decayed removable space.
*/
return free + removable;
}
protected int getWriters(PoolCostInfo info)
{
int writers = 0;
if (info.getStoreQueue() != null) {
writers += info.getStoreQueue().getWriters();
}
if (info.getRestoreQueue() != null) {
writers += info.getRestoreQueue().getWriters();
}
if (info.getP2pQueue() != null) {
writers += info.getP2pQueue().getWriters();
}
if (info.getP2pClientQueue() != null) {
writers += info.getP2pClientQueue().getWriters();
}
for (PoolCostInfo.PoolQueueInfo queue : info.getExtendedMoverHash().values()) {
writers += queue.getWriters();
}
return writers;
}
protected double getWeightedAvailable(PoolCostInfo info, double available, double load)
{
return (available == 0) ? 0 : (Math.pow(available, spaceCostFactor) / Math.pow(2.0, load));
}
private double getLoad(PoolCostInfo info)
{
return performanceCostFactor * info.getMoverCostFactor() * getWriters(info);
}
/**
* Selects a pool from a list using the WASS algorithm.
* <p/>
* Returns null if all pools are full.
*/
public <P> P selectByAvailableSpace(List<P> pools, long filesize,
Function<P, PoolCostInfo> getCost)
{
int length = pools.size();
double[] available = new double[length];
/* Calculate available space adjusted by space cost factor. Determine the smallest
* load of all pools able to hold the file.
*/
double minLoad = Double.POSITIVE_INFINITY;
for (int i = 0; i < length; i++) {
PoolCostInfo info = getCost.apply(pools.get(i));
double free = getAvailable(info.getSpaceInfo(), filesize);
if (free > 0) {
available[i] = free;
minLoad = Math.min(minLoad, getLoad(info));
}
}
if (minLoad == Double.POSITIVE_INFINITY) {
return null;
}
/* Weight available space by normalized load. Load is normalized to ensure that at least
* for one pool we maintain enough precision to not reduce available space to zero.
*/
double sum = 0.0;
for (int i = 0; i < length; i++) {
PoolCostInfo info = getCost.apply(pools.get(i));
double normalizedLoad = getLoad(info) - minLoad;
double weightedAvailable = getWeightedAvailable(info, available[i], normalizedLoad);
sum += weightedAvailable;
available[i] = sum;
}
/* Randomly choose one of the pools.
*/
double threshold = random() * sum;
for (int i = 0; i < length; i++) {
if (threshold < available[i]) {
return pools.get(i);
}
}
if (sum == Double.POSITIVE_INFINITY) {
throw new IllegalStateException("WASS overflow: Configured space cost factor (" + spaceCostFactor + ") is too large.");
}
throw new RuntimeException("Unreachable statement.");
}
}