package com.rubiconproject.oss.kv.distributed.impl; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.rubiconproject.oss.kv.distributed.Node; import com.rubiconproject.oss.kv.distributed.NodeChangeListener; import com.rubiconproject.oss.kv.distributed.NodeLocator; import com.rubiconproject.oss.kv.distributed.hashing.HashAlgorithm; import com.rubiconproject.oss.kv.distributed.hashing.HashRing; import com.rubiconproject.oss.kv.distributed.hashing.MD5HashAlgorithm; import com.rubiconproject.oss.kv.tuple.Tuple; import com.rubiconproject.oss.kv.tuple.Tuple2; /** * A node locator roughly comparable to Strategy 3 from the dynamo paper. * * @author sam * */ public class DynamoNodeLocator implements NodeLocator, NodeChangeListener { public static final int DEFAULT_TOKENS_PER_NODE = 100; private Log log = LogFactory.getLog(getClass()); private int tokensPerNode = DEFAULT_TOKENS_PER_NODE; private HashAlgorithm md5 = new MD5HashAlgorithm(); private volatile HashRing<Long, Token> outerRing; private volatile int maxNodeIterations = 0; private volatile int nodeCount = 0; public DynamoNodeLocator(int tokensPerNode) { this.tokensPerNode = tokensPerNode; } public DynamoNodeLocator() { } public List<Node> getPreferenceList(HashAlgorithm hashAlg, String key, int count) { if (count > outerRing.getNodeCount()) { String error = String.format( "Requested count (%1$d) is greater than node count (%2$d)", count, outerRing.getNodeCount()); IllegalArgumentException e = new IllegalArgumentException(error); log.error(error, e); throw e; } long hashCode = hashAlg.hash(key); List<Node> results = new ArrayList<Node>(count); Map.Entry<Long, Token> entry = outerRing.place(hashCode); results.add(entry.getValue().node); int stopLimit = 1; while ((results.size() < count) && (stopLimit < maxNodeIterations)) { entry = outerRing.lowerEntry(entry.getKey()); if (entry == null) entry = outerRing.lastEntry(); Node n = entry.getValue().node; // add if we do not have a node with this id, physical id boolean add = true; for (Node x : results) { if ((x.equals(n)) || (x.getPhysicalId() == (n.getPhysicalId()))) { add = false; break; } } if (add) results.add(n); ++stopLimit; } return results; } /** * Return the full ranked node list for a given key. * * @param hashAlg * @param key * @return */ public List<Node> getFullPreferenceList(HashAlgorithm hashAlg, String key) { return getPreferenceList(hashAlg, key, nodeCount); } /** * Return the primary token for a given key. Provided for unit testing. * * @param hashAlg * @param key * @return */ public int getPrimaryNode(HashAlgorithm hashAlg, String key) { if (outerRing.getNodeCount() == 0) throw new IllegalArgumentException("Ring is currently empty"); long hashCode = hashAlg.hash(key); Map.Entry<Long, Token> entry = outerRing.place(hashCode); return entry.getValue().id; } /** * Callback from the node store when the active node list has changed. * * @param nodes */ public void setActiveNodes(List<Node> nodes) { rebuild(nodes); } private void rebuild(List<Node> nodes) { if (nodes.size() == 0) throw new IllegalArgumentException("Unable to rebuild node list. Empty node list provided."); HashRing<Long, Token> newRing = new HashRing<Long, Token>(nodes.size()); // build the outer ring from Long.MIN_VALUE to Long.MAX_VALUE int tokenCount = tokensPerNode * newRing.getNodeCount(); long tokenSize = (Long.MAX_VALUE / tokenCount) * 2; for (int i = 1; i <= tokenCount; ++i) { long index = Long.MIN_VALUE + (i * tokenSize); Token token = new Token(i - 1, null); newRing.put(index, token); } List<Tuple2<Long, Node>> tokens = new ArrayList<Tuple2<Long, Node>>( newRing.size()); for (Node node : nodes) { for (int i = 1; i <= tokensPerNode; ++i) { // assign T tokens to this node String identifier = node.getSalt() + i; long hashCode = md5.hash(identifier); tokens.add(Tuple2.from(new Long(hashCode), node)); } } Collections.sort(tokens, new Comparator<Tuple2<Long, Node>>() { public int compare(Tuple2<Long, Node> o1, Tuple2<Long, Node> o2) { return Tuple.get1(o1).compareTo(Tuple.get1(o2)); } }); int i = 0; for (Token token : newRing.values()) { token.node = Tuple.get2(tokens.get(i)); ++i; } maxNodeIterations = tokensPerNode * nodes.size(); outerRing = newRing; nodeCount = nodes.size(); } private static class Token { private int id; private Node node; public Token(int id, Node node) { this.id = id; this.node = node; } } }