Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Array based ring, for ketama node locator lookups, for improved performance #9

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions src/main/java/net/spy/memcached/ArrayBasedCeilRing.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package net.spy.memcached;



import java.util.*;

/**
* Uses a sorted array of values to represent the consistent hash ring of values that are associated with
* memcached nodes are at that index.
*
* The method {@link #findCeilIndex(long)} finds the index in the array (the memcached node in the ring) at which the
* first value is greater than or equal to the given long is. If the long is greater than the maximum value
* in the array then the memcached node at the first position in the array is returned (The first item in the ring)
*/
public class ArrayBasedCeilRing {

private final long[] sortedNodePositions;
private final MemcachedNode[] sortedNodes;
private final Collection<MemcachedNode> allNodes;
private final int lastIndexPosition;

/**
* Is Passed ({@see #nodes}) a map of longs to the associated memcached nodes.
* This is used to create the sorted array ring and associated
* memcached nodes at those array indexes.
*
* The {@see #allNodes} is a list of unique memcached nodes that make up the current cluster
*
* @param nodes A map of longs to memcached nodes that should populate the consistent hash ring.
* @param allNodes The unique list of memcached nodes that are represented in the ring.
*/
public ArrayBasedCeilRing(Map<Long, MemcachedNode> nodes, Collection<MemcachedNode> allNodes) {
long[] sortedNodePositions = new long[nodes.size()];
MemcachedNode[] sortedNodes = new MemcachedNode[nodes.size()];
this.allNodes = new ArrayList(allNodes);

Long[] sortedObjectNodePositions = nodes.keySet().toArray(new Long[nodes.size()]);
Arrays.sort(sortedObjectNodePositions);

int i = 0;
for(Long position : sortedObjectNodePositions) {
sortedNodePositions[i] = position;
sortedNodes[i++] = nodes.get(position);
}

this.sortedNodePositions = sortedNodePositions;
this.sortedNodes = sortedNodes;
this.lastIndexPosition = nodes.size()-1;
}

/**
* Returns the max value in the ring that a memcached node is located at.
*/
public long getMaxPosition() {
return sortedNodePositions[lastIndexPosition];
}

/**
* Returns as a map, the memcached nodes associated to their positions
* @return
*/
public Map<Long,MemcachedNode> asMap() {
Map<Long,MemcachedNode> map = new TreeMap<Long,MemcachedNode>();
for(int i=0;i<sortedNodePositions.length;i++) {
map.put(sortedNodePositions[i],sortedNodes[i]);
}
return map;
}

/**
* Returns the list of memcached nodes that are represented in the current ring.
* @return
*/
public Collection<MemcachedNode> getAllNodes() {
return allNodes;
}

public ArrayBasedCeilRing roClone() {
Map<Long,MemcachedNode> nodes = new HashMap<Long, MemcachedNode>(sortedNodePositions.length,1.0f);
for(int i=0;i<sortedNodePositions.length;i++) {
nodes.put(sortedNodePositions[i],new MemcachedNodeROImpl(sortedNodes[i]));
}
List<MemcachedNode> allNodes = new ArrayList<MemcachedNode>(this.allNodes.size());
for(MemcachedNode node : this.allNodes) {
allNodes.add(new MemcachedNodeROImpl(node));
}

return new ArrayBasedCeilRing(nodes,allNodes);
}

public MemcachedNode findClosestNode(long key) {
return sortedNodes[findCeilIndex(key)];
}

/**
* Find the index in the array at which the first value greater than or equal
* to the given hashVal is. If hashVal is greater than the maximum value in the
* array then 0 is returned (The first item in the ring). If hashVal is less than or
* equal to the first element in the array then 0 is returned (The first item in the ring).
*
* Uses a binary search type algorithm ( O(log n) ) to find the closest largest value in the array, to the
* given hashVal
*
* @param hashVal The value to find the closest item
* @return The index in the array that the closest largest item exists
*/
public int findCeilIndex(final long hashVal) {
return findCeilIndex(hashVal,sortedNodePositions,lastIndexPosition);
}

/**
* Find the index in the array at which the first value greater than or equal
* to the given hashVal is. If hashVal is greater than the maximum value in the
* array then 0 is returned (The first item in the ring). If hashVal is less than or
* equal to the first element in the array then 0 is returned (The first item in the ring).
*
* Uses a binary search type algorithm ( O(log n) ) to find the closest largest value in the array, to the
* given hashVal
*
* @param hashVal The value to find the closest item
* @return The index in the array that the closest largest item exists
*/
static int findCeilIndex(final long hashVal, final long sortedNodePositions[], final int lastIndexPosition) {
if (lastIndexPosition < 0) {
throw new IllegalArgumentException("array cannot be empty");
}

int low = 0;
int high = lastIndexPosition;

// Check for edge cases
if (hashVal > sortedNodePositions[high]) return 0;
if (hashVal <= sortedNodePositions[low]) return 0;


while(true) {
// div by two
int mid = (low + high) >>> 1;
long midVal = sortedNodePositions[mid];
if (midVal == hashVal) return mid;
else if (midVal < hashVal) {
low = mid + 1;
if (sortedNodePositions[low] >= hashVal) return low;
}
else {
high = mid - 1;
if (sortedNodePositions[high] < hashVal) return mid;
}
}
}


}
61 changes: 61 additions & 0 deletions src/main/java/net/spy/memcached/ArrayBasedCeilRingIterator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package net.spy.memcached;

import java.util.Iterator;

/**
* Implements an Iterator which the KetamaNodeLoctaor may return to a client for
* iterating through alternate nodes for a given key.
*/
public class ArrayBasedCeilRingIterator implements Iterator<MemcachedNode> {

private final String key;
private long hashVal;
private int remainingTries;
private int numTries = 0;
private final HashAlgorithm hashAlg;
private final ArrayBasedCeilRing ring;

/**
* Create a new ArrayBasedCeilRingIterator to be used by a client for an operation.
*
* @param k the key to iterate for
* @param t the number of tries until giving up
* @param hashAlg the hash algorithm to use when selecting within the
* continuumq
*/
protected ArrayBasedCeilRingIterator(final String k, final int t,
final HashAlgorithm hashAlg, final ArrayBasedCeilRing ring) {
super();
this.ring = ring;
this.hashAlg = hashAlg;
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 ring.findClosestNode(hashVal);
} finally {
nextHash();
}
}

public void remove() {
throw new UnsupportedOperationException("remove not supported");
}

}
60 changes: 14 additions & 46 deletions src/main/java/net/spy/memcached/KetamaNodeLocator.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
Expand All @@ -49,8 +48,7 @@
*/
public final class KetamaNodeLocator extends SpyObject implements NodeLocator {

private volatile TreeMap<Long, MemcachedNode> ketamaNodes;
private volatile Collection<MemcachedNode> allNodes;
private volatile ArrayBasedCeilRing ketamaNodes;

private final HashAlgorithm hashAlg;
private final Map<InetSocketAddress, Integer> weights;
Expand Down Expand Up @@ -112,37 +110,35 @@ public KetamaNodeLocator(List<MemcachedNode> nodes, HashAlgorithm alg,
* continuum
* @param alg The hash algorithm to use when choosing a node in the Ketama
* consistent hash continuum
* @param weights node weights for ketama, a map from InetSocketAddress to
* @param nodeWeights node weights for ketama, a map from InetSocketAddress to
* weight as Integer
* @param configuration node locator configuration
*/
public KetamaNodeLocator(List<MemcachedNode> nodes, HashAlgorithm alg,
Map<InetSocketAddress, Integer> nodeWeights,
KetamaNodeLocatorConfiguration configuration) {
super();
allNodes = nodes;
hashAlg = alg;
config = configuration;
weights = nodeWeights;
isWeightedKetama = !weights.isEmpty();
setKetamaNodes(nodes);
}

private KetamaNodeLocator(TreeMap<Long, MemcachedNode> smn,
Collection<MemcachedNode> an, HashAlgorithm alg,
Map<InetSocketAddress, Integer> nodeWeights,
KetamaNodeLocatorConfiguration conf) {
private KetamaNodeLocator(ArrayBasedCeilRing nodes,
HashAlgorithm alg,
Map<InetSocketAddress, Integer> nodeWeights,
KetamaNodeLocatorConfiguration conf) {
super();
ketamaNodes = smn;
allNodes = an;
ketamaNodes = nodes;
hashAlg = alg;
config = conf;
weights = nodeWeights;
isWeightedKetama = !weights.isEmpty();
}

public Collection<MemcachedNode> getAll() {
return allNodes;
return ketamaNodes.getAllNodes();
}

public MemcachedNode getPrimary(final String k) {
Expand All @@ -152,61 +148,33 @@ public MemcachedNode getPrimary(final String k) {
}

long getMaxKey() {
return getKetamaNodes().lastKey();
return ketamaNodes.getMaxPosition();
}

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 = getKetamaNodes().tailMap(hash);
if (tailMap.isEmpty()) {
hash = getKetamaNodes().firstKey();
} else {
hash = tailMap.firstKey();
}
}
rv = getKetamaNodes().get(hash);
return rv;
return ketamaNodes.findClosestNode(hash);
}

public Iterator<MemcachedNode> getSequence(String k) {
// Seven searches gives us a 1 in 2^7 chance of hitting the
// same dead node all of the time.
return new KetamaIterator(k, 7, getKetamaNodes(), hashAlg);
return new ArrayBasedCeilRingIterator(k, 7, hashAlg,ketamaNodes);
}

public NodeLocator getReadonlyCopy() {
TreeMap<Long, MemcachedNode> smn =
new TreeMap<Long, MemcachedNode>(getKetamaNodes());
Collection<MemcachedNode> an =
new ArrayList<MemcachedNode>(allNodes.size());

// Rewrite the values a copy of the map.
for (Map.Entry<Long, MemcachedNode> me : smn.entrySet()) {
smn.put(me.getKey(), new MemcachedNodeROImpl(me.getValue()));
}

// Copy the allNodes collection.
for (MemcachedNode n : allNodes) {
an.add(new MemcachedNodeROImpl(n));
}

return new KetamaNodeLocator(smn, an, hashAlg, weights, config);
return new KetamaNodeLocator(ketamaNodes.roClone(), hashAlg, weights, config);
}

@Override
public void updateLocator(List<MemcachedNode> nodes) {
allNodes = nodes;
setKetamaNodes(nodes);
}

/**
* @return the ketamaNodes
*/
protected TreeMap<Long, MemcachedNode> getKetamaNodes() {
return ketamaNodes;
return (TreeMap)ketamaNodes.asMap();
}

/**
Expand Down Expand Up @@ -260,7 +228,7 @@ protected void setKetamaNodes(List<MemcachedNode> nodes) {
}
}
assert newNodeMap.size() == numReps * nodes.size();
ketamaNodes = newNodeMap;
ketamaNodes = new ArrayBasedCeilRing(newNodeMap,nodes);
}

private List<Long> ketamaNodePositionsAtIteration(MemcachedNode node, int iteration) {
Expand Down
Loading