package net.spy.memcached; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.SortedMap; import java.util.TreeMap; import net.spy.SpyObject; /** * This is an implementation of the Ketama consistent hash strategy from * last.fm. This implementation may not be compatible with libketama as * hashing is considered separate from node location. * * Note that this implementation does not currently supported weighted nodes. * * @see http://www.last.fm/user/RJ/journal/2007/04/10/392555/ */ public final class KetamaNodeLocator extends SpyObject implements NodeLocator { static final int NUM_REPS = 160; final SortedMap<Long, MemcachedNode> ketamaNodes= new TreeMap<Long, MemcachedNode>(); final Collection<MemcachedNode> allNodes; final HashAlgorithm hashAlg; public KetamaNodeLocator(List<MemcachedNode> nodes, HashAlgorithm alg) { super(); allNodes = nodes; hashAlg = alg; for(MemcachedNode node : nodes) { // XXX: Replace getSocketAddress() with something more precise String sockStr=String.valueOf(node.getSocketAddress()); // Ketama does some special work with md5 where it reuses chunks. if(alg == HashAlgorithm.KETAMA_HASH) { for(int i=0; i<NUM_REPS / 4; i++) { byte[] digest=HashAlgorithm.computeMd5(sockStr + "-" + i); for(int h=0;h<4;h++) { Long k = ((long)(digest[3+h*4]&0xFF) << 24) | ((long)(digest[2+h*4]&0xFF) << 16) | ((long)(digest[1+h*4]&0xFF) << 8) | (digest[h*4]&0xFF); ketamaNodes.put(k, node); } } } else { for(int i=0; i<NUM_REPS; i++) { ketamaNodes.put(hashAlg.hash(sockStr + "-" + i), node); } } } assert ketamaNodes.size() == NUM_REPS * nodes.size(); } public Collection<MemcachedNode> getAll() { return allNodes; } public MemcachedNode getPrimary(final String k) { MemcachedNode rv=getNodeForKey(hashAlg.hash(k)); assert rv != null : "Found no node for key " + k; return rv; } long getMaxKey() { return ketamaNodes.lastKey(); } MemcachedNode getNodeForKey(long hash) { final MemcachedNode rv; if(!ketamaNodes.containsKey(hash)) { // Java 1.6 adds a ceilingKey method, but I'm still stuck in 1.5 // in a lot of places, so I'm doing this myself. SortedMap<Long, MemcachedNode> tailMap=ketamaNodes.tailMap(hash); if(tailMap.isEmpty()) { hash=ketamaNodes.firstKey(); } else { hash=tailMap.firstKey(); } } rv=ketamaNodes.get(hash); return rv; } public Iterator<MemcachedNode> getSequence(String k) { return new KetamaIterator(k, allNodes.size()); } class KetamaIterator implements Iterator<MemcachedNode> { final String key; long hashVal; int remainingTries; int numTries=0; public KetamaIterator(final String k, final int t) { super(); hashVal=hashAlg.hash(k); remainingTries=t; key=k; } private void nextHash() { // this.calculateHash(Integer.toString(tries)+key).hashCode(); long tmpKey=hashAlg.hash((numTries++) + key); // This echos the implementation of Long.hashCode() hashVal += (int)(tmpKey ^ (tmpKey >>> 32)); hashVal &= 0xffffffffL; /* truncate to 32-bits */ remainingTries--; } public boolean hasNext() { return remainingTries > 0; } public MemcachedNode next() { try { return getNodeForKey(hashVal); } finally { nextHash(); } } public void remove() { throw new UnsupportedOperationException("remove not supported"); } } }