/* * JBoss, Home of Professional Open Source * Copyright 2010 Red Hat Inc. and/or its affiliates and other * contributors as indicated by the @author tags. All rights reserved. * See the copyright.txt in the distribution for a full listing of * individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.infinispan.distribution.ch; import org.infinispan.marshall.AbstractExternalizer; import org.infinispan.marshall.Ids; import org.infinispan.remoting.transport.Address; import org.infinispan.util.Util; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import static java.lang.Math.min; /** * <a href = "http://weblogs.java.net/blog/tomwhite/archive/2007/11/consistent_hash.html">Consistent hashing * algorithm</a>. Each target is entered into the pool <i>weight[i]</i>*<i>weightFactor</i> times. Where * <i>weight[i]</i> and <i>weightFactor</i> are integers greater than zero. * <p/> * Based on akluge's impl on <a href-="http://www.vizitsolutions.com/ConsistentHashingCaching.html">http://www.vizitsolutions.com/ConsistentHashingCaching.html</a> * * @author akluge * @author Manik Surtani */ public class ExperimentalDefaultConsistentHash extends AbstractConsistentHash { /** * A Weight and weight factor of 1 gives one node per address. In future we may decide to make these configurable, * to allow for virtual nodes and a better spread of state across nodes, but we need to ensure we deal with backups * not falling on virtual nodes on the same cache instances first. */ private static final int DEFAULT_WEIGHT = 1; private static final int DEFAULT_WEIGHTFACTOR = 1; private List<Address> nodes; private List<Entry> pool; private int poolSize; public static class Externalizer extends AbstractExternalizer<ExperimentalDefaultConsistentHash> { @Override public void writeObject(ObjectOutput output, ExperimentalDefaultConsistentHash object) throws IOException { output.writeObject(object.nodes); } @Override @SuppressWarnings("unchecked") public ExperimentalDefaultConsistentHash readObject(ObjectInput input) throws IOException, ClassNotFoundException { List<Address> addresses = (List<Address>) input.readObject(); ExperimentalDefaultConsistentHash gch = new ExperimentalDefaultConsistentHash(); gch.setCaches(addresses); return gch; } @Override public Integer getId() { return Ids.DEFAULT_CONSISTENT_HASH; } @Override public Set<Class<? extends ExperimentalDefaultConsistentHash>> getTypeClasses() { return Util.<Class<? extends ExperimentalDefaultConsistentHash>>asSet(ExperimentalDefaultConsistentHash.class); } } @Override public Set<Address> getCaches() { return new LinkedHashSet<Address>(nodes); } @Override public void setCaches(Set<Address> caches) { setCaches((Collection<Address>)caches); } public void setCaches(Collection<Address> caches) { nodes = new ArrayList<Address>(caches); int numNodes = nodes.size(); int poolSize = 0; for (int i = 0; i < numNodes; i++) { poolSize += DEFAULT_WEIGHT * DEFAULT_WEIGHTFACTOR; } this.poolSize = poolSize; pool = new ArrayList<Entry>(poolSize); int numEntries = 0; for (int i = 0; i < numNodes; i++) { numEntries = add(nodes.get(i), DEFAULT_WEIGHT * DEFAULT_WEIGHTFACTOR, numEntries); } Collections.sort(pool); nodes = getSortedCachesList(); } private List<Address> getSortedCachesList() { ArrayList<Address> caches = new ArrayList<Address>(); for (Entry e : pool) { if (!caches.contains(e.address)) caches.add(e.address); } caches.trimToSize(); return caches; } /** * Adds an Address to the pool of available addresses. * * @param node Address to be added to the hash pool. * @param count An int giving the number of times the node is added to the pool. The count is greater than zero, * and is likely greater than 10 if weights are used. The count for the i-th node is usually the * <i>weights</i>[i]*<i>weightfactor</i>, if weights are used. * @param position The position in the pool to begin adding node entries. * @return position; */ private int add(Address node, int count, int position) { int hash; String nodeName = node.toString(); for (int i = 0; i < count; i++) { hash = hash((Integer.toString(i) + nodeName).getBytes(Charset.forName("UTF-8"))); pool.add(position++, new Entry(node, nodeName, i, hash)); } return position; } /** * The distance between the first entries in the address array for two caches, a1 and a2. Of questionable use when * virtual nodes are employed. * * @param a1 The address of the first cache. * @param a2 The address of the second cache. * @return Am int containing the difference between these two indices. */ public int getDistance(Address a1, Address a2) { if (a1 == null || a2 == null) throw new NullPointerException("Cannot find the distance between null servers."); int p1 = nodes.indexOf(a1); if (p1 < 0) throw new IllegalArgumentException("Address " + a1 + " not in the addresses list of this consistent hash impl!"); int p2 = nodes.indexOf(a2); if (p2 < 0) throw new IllegalArgumentException("Address " + a2 + " not in the addresses list of this consistent hash impl!"); if (p1 <= p2) return p2 - p1; else return pool.size() - (p1 - p2); } /** * Two hashes are adjacent if they are next to each other in the consistent hash. * * @param a1 The address of the first cache. * @param a2 The address of the second cache. * @return A boolean, true if they are adjacent, false if not. */ public boolean isAdjacent(Address a1, Address a2) { int distance = getDistance(a1, a2); return distance == 1 || distance == pool.size() - 1; } @Override public List<Address> locate(Object key, int replCount) { if (key == null) throw new NullPointerException("Attempt to get with null key"); int clusterSize = pool.size(); int numCopiesToFind = min(replCount, clusterSize); int hashValue = hash(key); return locate(hashValue, numCopiesToFind, replCount); } /** * Returns a List of <i>numCopiesToFind</i> unique Addresses. * * @param hashValue An int, usually a hash, to be mapped to a bin via the CH. * @param numCopiesToFind number of copies to find * @param replCount replication count * @return Returns a List of <i>numCopiesToFind</i> unique Addresses. */ private List<Address> locate(int hashValue, int numCopiesToFind, int replCount) { // Stop looking if we have checked the entire pool. int checked = 0; // Start looking at the first (primary) node for entries for this value. int inode = findNearestNodeInPool(hashValue); List<Address> nodes = new ArrayList<Address>(numCopiesToFind); while (nodes.size() < replCount && checked < poolSize) { Entry poolEntry; if ((poolEntry = pool.get(inode)) != null && nodes.indexOf(poolEntry.address) < 0) { nodes.add(poolEntry.address); } inode = (++inode) % poolSize; checked++; } return nodes; } /** * Find a target for a hash key within the pool of node Entries. We search within a slice of the array bounded by * lowerBound and upperBound. Further we assume that lowerBound and upperBound are small enough that their sum will * not overflow an int. * <p/> * * @param hash The desired hash to locate. * @return An int giving the index of the desired entry in the list of targets. If the target is not found, then * -(lowerBound +1) will be returned, where lowerBound is the lower bound of the search after possibly * several iterations. */ private int binarySearch(int hash) { int lowerBound = 0; int upperBound = pool.size() - 1; 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 = pool.get(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 pool ArrayList such that the hash of the i-th entry >= hash. * * @param hash The hash being mapped to a bin via the consistent hash. * @return An int, the lowest index into the target array such that the hash of the i-th entry >= hash. */ private int findNearestNodeInPool(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 nodeIndex = binarySearch(hash); // If the returned value is less than zero, then no exact match was found. if (nodeIndex < 0) { // The value returned is -(lowerBound +1), we want the lower bound back. nodeIndex = -(nodeIndex + 1); // If hash is greater than the last entry, wrap around to the first. if (nodeIndex >= pool.size()) { nodeIndex = 0; } } return nodeIndex; } /** * Use the objects built in hash to obtain an initial value, then use a second four byte hash to obtain a more * uniform distribution of hash values. This uses a <a href = "http://burtleburtle.net/bob/hash/integer.html">4-byte * (integer) hash</a>, which produces well distributed values even when the original hash produces thghtly clustered * values. * <p/> * It is important that the object implement its own hashcode, and not use the Object hashcode. * * @param object object to hash * @return an appropriately spread hash code */ private int hash(Object object) { int hash = object.hashCode(); hash = (hash + 0x7ED55D16) + (hash << 12); hash = (hash ^ 0xc761c23c) ^ (hash >> 19); hash = (hash + 0x165667b1) + (hash << 5); hash = (hash + 0xd3a2646c) ^ (hash << 9); hash = (hash + 0xfd7046c5) + (hash << 3); hash = (hash ^ 0xb55a4f09) ^ (hash >> 16); return hash; } @Override public List<Integer> getHashIds(Address a) { throw new RuntimeException("Not yet implemented"); } /** * @return A String representing the object pool. */ @Override public String toString() { return " pool: " + pool; } @Override public boolean equals(Object other) { if (other == null || !(other instanceof ExperimentalDefaultConsistentHash)) { return false; } ExperimentalDefaultConsistentHash otherHash = (ExperimentalDefaultConsistentHash) other; return Util.safeEquals(pool, otherHash.pool); } @Override public int hashCode() { int hashCode = 1; for (Entry e : pool) hashCode = 31 * hashCode + e.hash; return hashCode; } /** * An entry into a consistent hash. It wraps the original object, the object's hash as used to generate the * consistent hash, the value extracted from the object used to generate the hash, and the modifier used to * differentiate the hash. */ public static class Entry implements Comparable<Entry> { public final int differentiator; public final int hash; public final Address address; public final String string; public Entry(Address address, String string, int differentiator, int hash) { this.differentiator = differentiator; this.hash = hash; this.address = address; this.string = string; } /** * Compare this Entry with another Entry. First the hash values are compared, then the differentiator is compared. * if the hash values are equal. * * @param other An Entry object to be compared with this object. Returns <ul> <li>-1 if this Entry is less than * the other Entry.</li> <li>0 if they are equal.</li> <li>+1 if this Entry is greater than the * other Entry.</li> </ul> * @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 && address.equals(otherEntry.address); } return false; } @Override public int hashCode() { return hash; } @Override public String toString() { return string + ":" + Integer.toHexString(hash); } } }