package org.infinispan.distribution;
import java.util.Arrays;
/**
* Consistent
* hashing algorithm. Each target is entered into the pool weight[i]*weightFactor
* times. Where weight[i] and weightFactor are integers greater than zero.
*
* @author akluge
* @version $Rev: $, $Date: $
*/
public class GenericConsistentHash
{
private static final int FNV_OFFSET_BASIS = (int)2166136261L;
private static final int FNV_PRIME = 16777619;
private final Hasher hasher;
private final T[] nodes;
private final Entry[] pool;
private final int poolSize;
private final int replication;
private final int weightFactor;
private final int[] weights;
public static class Builder
{
private static final Hasher DEFAULT_HASH = new ReHash();
private static final int DEFAULT_REPLICATION = 2;
private static final int DEFAULT_WEIGHT = 1;
private static final int DEFAULT_WEIGHTFACTOR = 30;
private Hasher hasher;
private final T[] nodes;
private int replication;
private int weightFactor;
private int[] weights;
/**
* Create a consistent hash Builder with the given set of nodes.
* Defaults are used for replication and weights.
*
* @param nodes An array of objects to me entered into a consistent hash.
*/
public Builder(T[] nodes)
{
hasher = DEFAULT_HASH;
this.nodes = nodes;
replication = DEFAULT_REPLICATION;
weightFactor = DEFAULT_WEIGHTFACTOR;
weights = new int[this.nodes.length];
Arrays.fill(weights, DEFAULT_WEIGHT);
}
/**
* @param replication An int giving the number of nodes to be returned
* by the get method.
*
* @return The Builder with the replication value set.
*
* @throws IllegalArgumentException If replication is less than 1. You must return
* at least one item with each get call.
*/
public Builder replication(int replication)
{
if (replication < 1) {
throw new IllegalArgumentException("The minimum replication is 1.");
}
this.replication = replication;
return this;
}
/**
* @param weights An array of integers controling the number of times
* each node is entered into the hash pool. The ith node
* occures weights[i]*weightfactor times
* in the pool.
*
* @return The Builder with the weights set.
*
* @throws IllegalArgumentException If the weights and the nodes arrays are not
* the same size.
*
* @throws IllegalArgumentException If any of the weights are less than zero.
*/
public Builder weights(int[] weights)
{
if (weights.length != nodes.length)
{
throw new IllegalArgumentException("The weights and nodes arrays are of different sizes."
+ "They must be of the same size.");
}
int nweights = weights.length;
for (int i=0; i weightFactor(int weightFactor)
{
if (weightFactor < 1)
{
throw new IllegalArgumentException("The minimum weight factor is one.");
}
this.weightFactor = weightFactor;
return this;
}
/**
* Provide a strategy for computing the hash of the keys. This must provide
* a hash method which produces a uniform distribution of values given a random
* object.
*
* @param hasher Implements the {@link Hasher} interface.
*
* @return The Builder with the hasher set.
*/
public Builder hasher(Hasher hasher)
{
if (hasher == null)
{
throw new IllegalArgumentException("A null hash is not allowed.");
}
this.hasher = hasher;
return this;
}
/**
* Construct a ConsistentHash from the settings provided to the Builder.
*
* @return A GenericConsistentHash.
*/
public GenericConsistentHash build()
{
return new GenericConsistentHash(this);
}
}
/**
* Build a consistent hash as described by the builder.
*
* @param builder A Bilder instance.
*/
private GenericConsistentHash(Builder builder)
{
hasher = builder.hasher;
nodes = builder.nodes;
replication = builder.replication;
weightFactor = builder.weightFactor;
weights = builder.weights;
int nnodes = nodes.length;
int poolSize = 0;
for (int i=0; i[])new Entry[this.poolSize];
int nentries = 0;
for (int i=0; iweights[i]*weightfactor
* @param pool The expanded set of all nodes mapped multiple times into the
* range of the hash function.
* @param position The position in the pool to begin adding node entries.
*
* @return position;
*/
private int add(T node, int count, Entry[] pool, int position)
{
Entry entry;
int hash;
String nodeName = node.toString();
for (int i = 0; i < count; i++)
{
hash = hash((Integer.toString(i) + nodeName).getBytes());
//System.out.print(" Hash: " + inode);
entry = new Entry(node, nodeName, i, hash);
pool[position++] = entry;
}
return position;
}
/**
* Removes a node of type T from the pool of nodes. This requires the location
* and removal of weights[i]*weightfactor copies of the node from
* the pool. The entries corresponding to the given node are set to null.
*
* @param node A generic node of type T to be removed from the pool.
*/
public void remove(T node)
{
int inode = find(nodes, node);
// The number of expected instances of this node in the pool.
int count = weights[inode]*weightFactor;
int hash;
for (int i = 0; i < count; i++)
{
// Find the ith occurance of the node.
hash = hash((node.toString() + i).getBytes());
inode = binarySearch(pool, 0, poolSize-1, hash);
if (inode >= 0)
{
pool[inode] = null;
} // Eventually, throw exception here?? Something has gone very wrong.
}
}
/**
* Get the number of entries we return for get request.
*
* @return An int giving the number of nodes we return with each get.
*/
public int getReplication()
{
return replication;
}
/**
* Returns and array containing replication unique nodes for a key.
*
* @param key An object used as a key in the hash. The object's toString method
* is called, and a fast hash is used to compute the hash. The toString
* method on the key class must present the variables that contain the
* relevant state of the object.
*
* @param nodes An array of the paramaterized type T, which must match the original
* type used in creating the consistent hash, and whose size must match
* the replication count.
* @see #getReplication()
*
* @return An array containing replication objects of the paramaterized
* type T.
*/
public T[] locate(Object key, T[] nodes)
{
if (key == null)
{
throw new NullPointerException("Attempt to get with null key");
}
if (nodes.length < replication)
{
throw new IllegalArgumentException("Nodes must contain at least "
+ replication + " entries.");
}
int hashValue = hasher.hash(key);
return locate(hashValue, nodes, replication);
}
/**
* Returns and array containing the requested count of unique nodes for a key.
*
* @param key An object used as a key in the hash. By default, the key's
* hashCode is used, so it is important that this method be
* implemented.
*
* @param nodes An array of the paramaterized type T, which must match the original
* type used in creating the consistent hash, and whose size must match
* the replication count.
*
* @oaram count An int giving the number of node to be returned.
*
* @return An array containing count objects of the paramaterized
* type T.
*/
public T[] locate(Object key, T[] nodes, int count)
{
if (key == null)
{
throw new NullPointerException("Attempt to get with null key");
}
if (nodes.length < count)
{
throw new IllegalArgumentException("Nodes must contain at least "
+ count + " entries.");
}
if (count > this.nodes.length)
{
throw new IllegalArgumentException();
}
int hashValue = hasher.hash(key);
return locate(hashValue, nodes, count);
}
/**
* Returns and array containing replication unique nodes for a value.
*
* @param value An int, usually a hash, to be mapped to a bin via the CH.
*
* @param nodes An array of the paramaterized type T, which must match the original
* type used in creating the consistent hash, and whose size must match
* the replication count.
* @see #getReplication()
*
* @return An array containing replication objects of the paramaterized
* type T.
*/
public T[] locate(int value, T[] nodes, int count)
{
// Stop looking if we have checked the entire pool.
int checked = 0;
// Or if we have found as many instances as we need.
int found = 0;
// Start looking at the first (primary) node for entries for this value.
int inode = findNearest(pool, value);
//System.out.println(key + " : " + hash(key.toString().getBytes()) + " : " + inode);
while (found < count && checked < poolSize)
{
if (pool[inode] != null && find(nodes, pool[inode].object)<0)
{
nodes[found++] = pool[inode].object;
}
inode = (++inode)%poolSize;
checked++;
}
return nodes;
}
/**
* Return the single node that owns the key. Used to validate the destination of
* the primary request.
*
* @param value An int, usually representing a hash. Locate the bin, or node,
* that owns the int.
*
* @return A node that owns the given int.
*/
public T locate(int value)
{
return pool[findNearest(pool, value)].object;
}
/**
* The number of bins in the consistent hash.
* @return An int giving the number of items in the consistent hash.
*/
public int getPoolSize()
{
return poolSize;
}
/**
* Get the objects that define the consistent hash, in the order they are sorted into for the hash.
*
* @param values An array ot type T to hold the original values, now duplicated (by weight) and sorted.
* @return An array of type T.
*/
public T[] getPoolValues(T[] values)
{
if (values.length != poolSize)
{
throw new IllegalArgumentException("Size mismatch in values array. Expected " + poolSize + " got " + values.length);
}
for (int i=0; iFNV 1
* hash. If needed, consider a faster
* hash. This is a fast hash with a good distribution, which is all that matters in this
* case.
*
* @param bytes The array of bytes to be hashed.
* @return An int containing the FNV hash of the byte array.
*/
public int hash(byte[] bytes)
{
int nbytes = bytes.length;
int hash = FNV_OFFSET_BASIS;
for(int i=0; i[] targets, int lowerBound,
int upperBound, int hash)
{
while (lowerBound <= upperBound)
{
// Fast div by 2. We assume that the number of targets is small enough
// that the sum will not overflow an int.
int mid = (lowerBound + upperBound) >>> 1;
int currentHash = targets[mid].hash;
if (currentHash < hash)
{
lowerBound = mid + 1;
}
else if (currentHash > hash)
{
upperBound = mid - 1;
}
else
{
return mid;
}
}
// The +1 ensures that the return value is negative, even when the hash
// is off the left edge of the array.
return -(lowerBound +1);
}
/**
* Finds the lowest index into the targets array such that T[i] >= hash.
*
* @param targets The list of targets over which we maintain the consistent hash.
* @param hash The hash being maped to a bin via the consistent hash.
*
* @return An int, the lowest index into the target array such that T[i] >= hash.
*/
public int findNearest(Entry[] targets, int hash)
{
// Find the index of the node - or at least a near one.
// We only search up to targets.length-1. If the element
// is greater than the last entry in the list, then map
// it to the first one.
int inode = binarySearch(targets, 0, targets.length -1, hash);
// If the returned value is less than zero, then no exact match was found.
if (inode < 0)
{
// The value returned is -(lowerBound +1), we want the lower bound back.
inode = -(inode + 1);
// If hash is greater than the last entry, wrap around to the first.
if (inode >= targets.length)
{
inode = 0;
}
}
return inode;
}
/**
* Linear search for a target in a list of targets.
*
* @param nodes An array of the paramaterized type T.
* @param target An entry for T objects.
*
* @return An int giving the location of the target in the array
* of nodes, or -1 if not found.
*/
private int find(T[] nodes, T target)
{
int nnodes = nodes.length;
for (int i=0; i implements Comparable
{
public final int differentiator;
public final int hash;
public final T object;
public final String string;
public Entry(T object, String string, int differentiator, int hash)
{
this.differentiator = differentiator;
this.hash = hash;
this.object = object;
this.string = string;
}
/**
* Compare this Entry with another Entry. First the hash values
* are compaired, then the differentiator is compaired. if the
* hash values are equal.
*
* @param other An Entry object to be compared with this object. Returns
*
* - -1 if this Entry is less than the other Entry.
* - 0 if they are equal.
* - +1 if this Entry is greater than the other Entry.
*
*
* @return
*/
//@Override
public int compareTo(Entry other)
{
if (this.hash < other.hash)
{
return -1;
}
if (this.hash > other.hash)
{
return 1;
}
if (this.differentiator < other.differentiator)
{
return -1;
}
if (this.differentiator > other.differentiator)
{
return +1;
}
return 0;
}
@Override
public boolean equals(Object other)
{
if (other instanceof Entry)
{
Entry otherEntry = (Entry)other;
return hash == otherEntry.hash
&& differentiator == otherEntry.differentiator
&& object.equals(otherEntry.object);
}
return false;
}
@Override
public int hashCode()
{
return hash;
}
@Override
public String toString()
{
return string + ":" + Integer.toHexString(hash);
}
}
}