package com.netflix.evcache.pool;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import net.spy.memcached.DefaultHashAlgorithm;
import net.spy.memcached.EVCacheMemcachedNodeROImpl;
import net.spy.memcached.HashAlgorithm;
import net.spy.memcached.MemcachedNode;
import net.spy.memcached.NodeLocator;
import net.spy.memcached.util.KetamaNodeLocatorConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.config.ChainedDynamicProperty;
import com.netflix.evcache.util.EVCacheConfig;
public class EVCacheNodeLocator implements NodeLocator {
private static Logger log = LoggerFactory.getLogger(EVCacheNodeLocator.class);
private TreeMap<Long, MemcachedNode> ketamaNodes;
private final String appName;
private final ServerGroup serverGroup;
private ChainedDynamicProperty.BooleanProperty partialStringHash;
private ChainedDynamicProperty.StringProperty hashDelimiter;
private final Collection<MemcachedNode> allNodes;
private final HashAlgorithm hashingAlgorithm;
private final KetamaNodeLocatorConfiguration config;
/**
* Create a new KetamaNodeLocator using specified nodes and the specifed
* hash algorithm and configuration.
*
* @param nodes
* The List of nodes to use in the Ketama consistent hash
* continuum
* @param alg
* The hash algorithm to use when choosing a node in the Ketama
* consistent hash continuum
* @param conf
*/
public EVCacheNodeLocator(String appName, ServerGroup serverGroup, List<MemcachedNode> nodes, HashAlgorithm alg, KetamaNodeLocatorConfiguration conf) {
super();
this.allNodes = nodes;
this.hashingAlgorithm = alg;
this.config = conf;
this.appName = appName;
this.serverGroup = serverGroup;
this.partialStringHash = EVCacheConfig.getInstance().getChainedBooleanProperty("EVCacheNodeLocator." + appName+ ".hash.on.partial.key", "EVCacheNodeLocator." + appName + "." + serverGroup.getName() + ".hash.on.partial.key", Boolean.FALSE);
this.hashDelimiter = EVCacheConfig.getInstance().getChainedStringProperty("EVCacheNodeLocator." + appName + ".hash.delimiter", "EVCacheNodeLocator." + appName + "." + serverGroup.getName() + ".hash.delimiter", ":");
setKetamaNodes(nodes);
}
private EVCacheNodeLocator(String appName, ServerGroup serverGroup, TreeMap<Long, MemcachedNode> smn, Collection<MemcachedNode> an, HashAlgorithm alg, KetamaNodeLocatorConfiguration conf) {
super();
this.ketamaNodes = smn;
this.allNodes = an;
this.hashingAlgorithm = alg;
this.config = conf;
this.appName = appName;
this.serverGroup = serverGroup;
this.partialStringHash = EVCacheConfig.getInstance().getChainedBooleanProperty("EVCacheNodeLocator." + appName + ".hash.on.partial.key", "EVCacheNodeLocator." + appName + "." + serverGroup.getName() + ".hash.on.partial.key", Boolean.FALSE);
this.hashDelimiter = EVCacheConfig.getInstance().getChainedStringProperty("EVCacheNodeLocator." + appName + ".hash.delimiter", "EVCacheNodeLocator." + appName + "." + serverGroup.getName() + ".hash.delimiter", ":");
}
/*
* @see net.spy.memcached.NodeLocator#getAll
*/
public Collection<MemcachedNode> getAll() {
return allNodes;
}
/*
* @see net.spy.memcached.NodeLocator#getPrimary
*/
public MemcachedNode getPrimary(String k) {
if (partialStringHash.get()) {
final int index = k.indexOf(hashDelimiter.get());
if (index > 0) {
k = k.substring(0, index);
}
}
final long _hash = hashingAlgorithm.hash(k);
Long hash = Long.valueOf(_hash);
hash = ketamaNodes.ceilingKey(hash);
if (hash == null) {
hash = ketamaNodes.firstKey();
}
return ketamaNodes.get(hash);
}
/*
* @return Returns the max key in the hashing distribution
*/
public long getMaxKey() {
return getKetamaNodes().lastKey().longValue();
}
public MemcachedNode getNodeForKey(long _hash) {
long start = (log.isDebugEnabled()) ? System.nanoTime() : 0;
try {
Long hash = Long.valueOf(_hash);
hash = ketamaNodes.ceilingKey(hash);
if (hash == null) {
hash = ketamaNodes.firstKey();
}
return ketamaNodes.get(hash);
} finally {
if (log.isDebugEnabled()) {
final long end = System.nanoTime();
log.debug("getNodeForKey : \t" + (end - start) / 1000);
}
}
}
public Iterator<MemcachedNode> getSequence(String k) {
final List<MemcachedNode> allKetamaNodes = new ArrayList<MemcachedNode>(getKetamaNodes().values());
Collections.shuffle(allKetamaNodes);
return allKetamaNodes.iterator();
}
public NodeLocator getReadonlyCopy() {
final TreeMap<Long, MemcachedNode> ketamaNaodes = new TreeMap<Long, MemcachedNode>(getKetamaNodes());
final Collection<MemcachedNode> aNodes = new ArrayList<MemcachedNode>(allNodes.size());
// Rewrite the values a copy of the map.
for (Map.Entry<Long, MemcachedNode> me : ketamaNaodes.entrySet()) {
me.setValue(new EVCacheMemcachedNodeROImpl(me.getValue()));
}
// Copy the allNodes collection.
for (MemcachedNode n : allNodes) {
aNodes.add(new EVCacheMemcachedNodeROImpl(n));
}
return new EVCacheNodeLocator(appName, serverGroup, ketamaNaodes, aNodes, hashingAlgorithm, config);
}
/**
* @return the ketamaNodes
*/
protected TreeMap<Long, MemcachedNode> getKetamaNodes() {
return ketamaNodes;
}
/**
* @return the readonly view of ketamaNodes. This is mailnly for admin
* purposes
*/
public Map<Long, MemcachedNode> getKetamaNodeMap() {
return Collections.<Long, MemcachedNode> unmodifiableMap(ketamaNodes);
}
/**
* Setup the KetamaNodeLocator with the list of nodes it should use.
*
* @param nodes
* a List of MemcachedNodes for this KetamaNodeLocator to use in
* its continuum
*/
protected final void setKetamaNodes(List<MemcachedNode> nodes) {
TreeMap<Long, MemcachedNode> newNodeMap = new TreeMap<Long, MemcachedNode>();
final int numReps = config.getNodeRepetitions();
for (MemcachedNode node : nodes) {
// Ketama does some special work with md5 where it reuses chunks.
if (hashingAlgorithm == DefaultHashAlgorithm.KETAMA_HASH) {
for (int i = 0; i < numReps / 4; i++) {
final String hashString = config.getKeyForNode(node, i);
byte[] digest = DefaultHashAlgorithm.computeMd5(hashString);
if (log.isDebugEnabled()) log.debug("digest : " + digest);
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);
newNodeMap.put(Long.valueOf(k), node);
if (log.isDebugEnabled()) log.debug("Key : " + hashString + " ; hash : " + k + "; node " + node );
}
}
} else {
for (int i = 0; i < numReps; i++) {
final Long hashL = Long.valueOf(hashingAlgorithm.hash(config.getKeyForNode(node, i)));
newNodeMap.put(hashL, node);
}
}
}
if (log.isDebugEnabled()) log.debug("NewNodeMapSize : " + newNodeMap.size() + "; MapSize : " + (numReps * nodes.size()));
if (log.isTraceEnabled()) {
for(Long key : newNodeMap.keySet()) {
if (log.isTraceEnabled()) log.trace("Hash : " + key + "; Node : " + newNodeMap.get(key));
}
}
ketamaNodes = newNodeMap;
}
@Override
public void updateLocator(List<MemcachedNode> nodes) {
setKetamaNodes(nodes);
}
@Override
public String toString() {
return "EVCacheNodeLocator [ketamaNodes=" + ketamaNodes + ", appName=" + appName + ", serverGroup="
+ serverGroup + ", partialStringHash=" + partialStringHash + ", hashDelimiter=" + hashDelimiter
+ ", allNodes=" + allNodes + ", hashingAlgorithm=" + hashingAlgorithm + ", config=" + config + "]";
}
}