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); } } }