net.spy.memcached.ArcusReplKetamaNodeLocator Maven / Gradle / Ivy
The newest version!
/*
* arcus-java-client : Arcus Java client
* Copyright 2010-2014 NAVER Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* ENABLE_REPLICATION if */
package net.spy.memcached;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import net.spy.memcached.compat.SpyObject;
import net.spy.memcached.util.ArcusReplKetamaNodeLocatorConfiguration;
public class ArcusReplKetamaNodeLocator extends SpyObject implements NodeLocator {
private final TreeMap> ketamaGroups;
private final HashMap allGroups;
private final Collection allNodes;
private final HashAlgorithm hashAlg;
private final ArcusReplKetamaNodeLocatorConfiguration config;
private final Lock lock = new ReentrantLock();
public ArcusReplKetamaNodeLocator(List nodes, HashAlgorithm alg) {
// This configuration class is aware that InetSocketAddress is really
// ArcusReplNodeAddress. Its getKeyForNode uses the group name, instead
// of the socket address.
this(nodes, alg, new ArcusReplKetamaNodeLocatorConfiguration());
}
private ArcusReplKetamaNodeLocator(List nodes, HashAlgorithm alg,
ArcusReplKetamaNodeLocatorConfiguration conf) {
super();
allNodes = nodes;
hashAlg = alg;
ketamaGroups = new TreeMap>();
allGroups = new HashMap();
config = conf;
// create all memcached replica group
for (MemcachedNode node : nodes) {
MemcachedReplicaGroup mrg =
allGroups.get(MemcachedReplicaGroup.getGroupNameForNode(node));
if (mrg == null) {
mrg = new MemcachedReplicaGroupImpl(node);
getLogger().info("new memcached replica group added %s", mrg.getGroupName());
allGroups.put(mrg.getGroupName(), mrg);
} else {
mrg.setMemcachedNode(node);
}
}
int numReps = config.getNodeRepetitions();
// Ketama does some special work with md5 where it reuses chunks.
if (alg == HashAlgorithm.KETAMA_HASH) {
for (MemcachedReplicaGroup group : allGroups.values()) {
updateHash(group, false);
}
} else {
for (MemcachedReplicaGroup group : allGroups.values()) {
for (int i = 0; i < numReps; i++) {
long nodeHashKey = hashAlg.hash(config.getKeyForGroup(group, i));
SortedSet nodeSet = ketamaGroups.get(nodeHashKey);
if (nodeSet == null) {
nodeSet = new TreeSet(
new ArcusReplKetamaNodeLocatorConfiguration.MemcachedReplicaGroupComparator());
ketamaGroups.put(nodeHashKey, nodeSet);
}
nodeSet.add(group);
}
}
}
assert ketamaGroups.size() <= (numReps * allGroups.size());
/* ketamaGroups.size() can be smaller than numReps*AllGroups.size()
* because of hash collision
*/
}
private ArcusReplKetamaNodeLocator(TreeMap> kg,
HashMap ag,
Collection an,
HashAlgorithm alg,
ArcusReplKetamaNodeLocatorConfiguration conf) {
super();
ketamaGroups = kg;
allGroups = ag;
allNodes = an;
hashAlg = alg;
config = conf;
}
public Collection getAll() {
return allNodes;
}
public Map getAllGroups() {
return allGroups;
}
public Collection getMasterNodes() {
List masterNodes = new ArrayList(allGroups.size());
for (MemcachedReplicaGroup g : allGroups.values()) {
masterNodes.add(g.getMasterNode());
}
return masterNodes;
}
public MemcachedNode getPrimary(final String k) {
return getNodeForKey(hashAlg.hash(k), ReplicaPick.MASTER);
}
public MemcachedNode getPrimary(final String k, ReplicaPick pick) {
return getNodeForKey(hashAlg.hash(k), pick);
}
public long getMaxKey() {
return ketamaGroups.lastKey();
}
private MemcachedNode getNodeForKey(long hash, ReplicaPick pick) {
MemcachedReplicaGroup rg;
MemcachedNode rv = null;
lock.lock();
try {
if (!ketamaGroups.isEmpty()) {
if (!ketamaGroups.containsKey(hash)) {
Long nodeHash = ketamaGroups.ceilingKey(hash);
if (nodeHash == null) {
hash = ketamaGroups.firstKey();
} else {
hash = nodeHash.longValue();
}
// 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 tailMap = ketamaNodes.tailMap(hash);
if (tailMap.isEmpty()) {
hash = ketamaNodes.firstKey();
} else {
hash = tailMap.firstKey();
}
*/
}
rg = ketamaGroups.get(hash).first();
// return a node (master / slave) for the replica pick request.
rv = rg.getNodeForReplicaPick(pick);
}
} catch (RuntimeException e) {
throw e;
} finally {
lock.unlock();
}
return rv;
}
public Iterator getSequence(String k) {
return new ReplKetamaIterator(k, ReplicaPick.MASTER, allGroups.size());
}
public Iterator getSequence(String k, ReplicaPick pick) {
return new ReplKetamaIterator(k, pick, allGroups.size());
}
public NodeLocator getReadonlyCopy() {
TreeMap> smg =
new TreeMap>(ketamaGroups);
HashMap ag =
new HashMap(allGroups.size());
Collection an = new ArrayList(allNodes.size());
lock.lock();
try {
// Rewrite the values a copy of the map
for (Map.Entry> mge : smg.entrySet()) {
SortedSet groupROSet =
new TreeSet(
new ArcusReplKetamaNodeLocatorConfiguration.MemcachedReplicaGroupComparator());
for (MemcachedReplicaGroup mrg : mge.getValue()) {
groupROSet.add(new MemcachedReplicaGroupROImpl(mrg));
}
mge.setValue(groupROSet);
}
// copy the allGroups collection.
for (Map.Entry me : allGroups.entrySet()) {
ag.put(me.getKey(), new MemcachedReplicaGroupROImpl(me.getValue()));
}
// copy the allNodes collection.
for (MemcachedNode n : allNodes) {
an.add(new MemcachedNodeROImpl(n));
}
} catch (RuntimeException e) {
throw e;
} finally {
lock.unlock();
}
return new ArcusReplKetamaNodeLocator(smg, ag, an, hashAlg, config);
}
public void update(Collection toAttach, Collection toDelete) {
update(toAttach, toDelete, new ArrayList(0));
}
public void update(Collection toAttach,
Collection toDelete,
Collection changeRoleGroups) {
/* We must keep the following execution order
* - first, remove nodes.
* - second, change role.
* - third, add nodes
*/
lock.lock();
try {
// Remove memcached nodes.
for (MemcachedNode node : toDelete) {
allNodes.remove(node);
MemcachedReplicaGroup mrg =
allGroups.get(MemcachedReplicaGroup.getGroupNameForNode(node));
mrg.deleteMemcachedNode(node);
try {
node.getSk().attach(null);
node.shutdown();
} catch (IOException e) {
getLogger().error("Failed to shutdown the node : " + node.toString());
node.setSk(null);
}
}
// Change role
for (MemcachedReplicaGroup g : changeRoleGroups) {
g.changeRole();
}
// Add memcached nodes.
for (MemcachedNode node : toAttach) {
allNodes.add(node);
MemcachedReplicaGroup mrg =
allGroups.get(MemcachedReplicaGroup.getGroupNameForNode(node));
if (mrg == null) {
mrg = new MemcachedReplicaGroupImpl(node);
getLogger().info("new memcached replica group added %s", mrg.getGroupName());
allGroups.put(mrg.getGroupName(), mrg);
updateHash(mrg, false);
} else {
mrg.setMemcachedNode(node);
}
}
// Delete empty group
List toDeleteGroup = new ArrayList();
for (Map.Entry entry : allGroups.entrySet()) {
MemcachedReplicaGroup group = entry.getValue();
if (group.isEmptyGroup()) {
toDeleteGroup.add(group);
}
}
for (MemcachedReplicaGroup group : toDeleteGroup) {
getLogger().info("old memcached replica group removed %s", group.getGroupName());
allGroups.remove(group.getGroupName());
updateHash(group, true);
}
} catch (RuntimeException e) {
throw e;
} finally {
lock.unlock();
}
}
public void switchoverReplGroup(MemcachedReplicaGroup group) {
lock.lock();
group.changeRole();
lock.unlock();
}
private void updateHash(MemcachedReplicaGroup group, boolean remove) {
// Ketama does some special work with md5 where it reuses chunks.
for (int i = 0; i < config.getNodeRepetitions() / 4; i++) {
byte[] digest = HashAlgorithm.computeMd5(config.getKeyForGroup(group, 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);
SortedSet nodeSet = ketamaGroups.get(k);
if (remove) {
nodeSet.remove(group);
if (nodeSet.size() == 0) {
ketamaGroups.remove(k);
}
} else {
if (nodeSet == null) {
nodeSet = new TreeSet(
new ArcusReplKetamaNodeLocatorConfiguration.MemcachedReplicaGroupComparator());
ketamaGroups.put(k, nodeSet);
}
nodeSet.add(group);
}
}
}
}
private class ReplKetamaIterator implements Iterator {
private final String key;
private long hashVal;
private int remainingTries;
private int numTries = 0;
private final ReplicaPick pick;
public ReplKetamaIterator(final String k, ReplicaPick p, final int t) {
super();
hashVal = hashAlg.hash(k);
remainingTries = t;
key = k;
pick = p;
}
private void nextHash() {
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, pick);
} finally {
nextHash();
}
}
public void remove() {
throw new UnsupportedOperationException("remove not supported");
}
}
}
/* ENABLE_REPLICATION end */