org.axonframework.commandhandling.distributed.ConsistentHash Maven / Gradle / Ivy
/*
* Copyright (c) 2010-2014. Axon Framework
*
* 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.
*/
package org.axonframework.commandhandling.distributed;
import org.axonframework.common.digest.Digester;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.StringWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* Basic implementation of a Consistent Hashing algorithm, using the MD5 algorithm to build the hash values for given
* keys and node names. It contains some basic operation to add nodes and remove nodes given a set of known remaining
* members.
*
* Each node contains a Set of supported Commands (as a set of the fully qualified names of payload types). When
* performing a lookup for a given command, only nodes that support the payload type of the command are eligible.
*
* @author Allard Buijze
* @since 2.0
*/
public class ConsistentHash implements Externalizable {
private static final long serialVersionUID = 799974496899291960L;
private static final ConsistentHash EMPTY = new ConsistentHash(new TreeMap());
private final SortedMap hashToMember;
/**
* Returns an instance of an empty Ring, which can be used to add members.
*
* @return an empty ConsistentHash ring.
*/
public static ConsistentHash emptyRing() {
return EMPTY;
}
/**
* Initializes an empty hash.
*
* This constructor is required for serialization. Instead of using this constructor, use {@link #emptyRing()} to
* obtain an instance.
*/
@SuppressWarnings("UnusedDeclaration")
public ConsistentHash() {
this(new TreeMap());
}
private ConsistentHash(SortedMap hashed) {
hashToMember = hashed;
}
/**
* Returns a ConsistentHash with the given additional nodeName
, which is given
* segmentCount
segments on the ring. A registration of a node will completely override any previous
* registration known for that node.
*
* @param nodeName The name of the node to add. This will be used to compute the segments
* @param segmentCount The number of segments to add the given node
* @param supportedCommandTypes The fully qualified names of command (payload) types this node supports
* @return a ConsistentHash with the given additional node
*/
public ConsistentHash withAdditionalNode(String nodeName, int segmentCount, Set supportedCommandTypes) {
TreeMap newHashes = new TreeMap(hashToMember);
Iterator> iterator = newHashes.entrySet().iterator();
while (iterator.hasNext()) {
if (nodeName.equals(iterator.next().getValue().name())) {
iterator.remove();
}
}
Member node = new Member(nodeName, segmentCount, supportedCommandTypes);
for (String key : node.hashes()) {
newHashes.put(key, node);
}
return new ConsistentHash(newHashes);
}
/**
* Returns a ConsistentHash instance where only segments leading to the given nodes
are available.
* Each
* lookup will always result in one of the given nodes
.
*
* @param nodes The nodes to keep in the consistent hash
* @return a ConsistentHash instance where only segments leading to the given nodes
are available
*/
public ConsistentHash withExclusively(Collection nodes) {
Set activeMembers = new HashSet(nodes);
SortedMap newHashes = new TreeMap();
for (Map.Entry entry : hashToMember.entrySet()) {
if (activeMembers.contains(entry.getValue().name())) {
newHashes.put(entry.getKey(), entry.getValue());
}
}
return new ConsistentHash(newHashes);
}
/**
* Returns the member for the given item
, that supports given commandType
. If no such
* member is available, this method returns null
.
*
* @param item The item to find a node name for
* @param commandType The type of command the member must support
* @return The node name for the given item
, or null
if not found
*/
public String getMember(String item, String commandType) {
String hash = Digester.md5Hex(item);
SortedMap tailMap = hashToMember.tailMap(hash);
Iterator> tailIterator = tailMap.entrySet().iterator();
Member foundMember = findSuitableMember(commandType, tailIterator);
if (foundMember == null) {
// if the tail doesn't have a member, we should start back at the head
Iterator> headIterator = hashToMember.headMap(hash).entrySet().iterator();
foundMember = findSuitableMember(commandType, headIterator);
}
return foundMember == null ? null : foundMember.name();
}
private Member findSuitableMember(String commandType, Iterator> iterator) {
Member foundMember = null;
while (iterator.hasNext() && foundMember == null) {
Map.Entry entry = iterator.next();
if (entry.getValue().supportedCommands().contains(commandType)) {
foundMember = entry.getValue();
}
}
return foundMember;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ConsistentHash ring = (ConsistentHash) o;
return hashToMember.equals(ring.hashToMember);
}
@Override
public int hashCode() {
return hashToMember.hashCode();
}
@Override
public String toString() {
StringWriter w = new StringWriter();
w.append("ConsistentHash: {");
Iterator> iterator = hashToMember.entrySet().iterator();
if (iterator.hasNext()) {
w.append("\n");
}
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
w.append(entry.getKey())
.append(" -> ")
.append(entry.getValue().name())
.append("(");
Iterator commandIterator = entry.getValue().supportedCommands().iterator();
while (commandIterator.hasNext()) {
w.append(commandIterator.next());
if (commandIterator.hasNext()) {
w.append(", ");
}
}
w.append(")");
if (iterator.hasNext()) {
w.append(", ");
}
w.append("\n");
}
w.append("}");
return w.toString();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
Set members = new HashSet(hashToMember.values());
out.writeInt(members.size());
for (Member node : members) {
out.writeUTF(node.name());
out.writeInt(node.segmentCount());
out.writeInt(node.supportedCommands().size());
for (String supportedCommand : node.supportedCommands()) {
out.writeUTF(supportedCommand);
}
}
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
int size = in.readInt();
for (int t = 0; t < size; t++) {
String memberName = in.readUTF();
int loadFactor = in.readInt();
int supportedCommandCount = in.readInt();
Set supportedCommands = new HashSet(supportedCommandCount);
for (int c = 0; c < supportedCommandCount; c++) {
supportedCommands.add(in.readUTF());
}
Member node = new Member(memberName, loadFactor, supportedCommands);
for (String key : node.hashes()) {
hashToMember.put(key, node);
}
}
}
/**
* Returns the set of members part of this hash ring.
*
* @return the set of members part of this hash ring
*/
public Set getMembers() {
return Collections.unmodifiableSet(new HashSet(hashToMember.values()));
}
/**
* Represents a member in a consistently hashed cluster. A member is identified by its name, supports a number of
* commands and can have any number of segments (a.k.a buckets).
*
* Note that a single member may be presented by multiple {@code Member} instances if the number of segments
* differs per supported command type.
*
* @author Allard Buijze
*/
public static class Member {
private final String nodeName;
private final Set supportedCommandTypes;
private final Set hashes;
/**
* Constructs a new member with given nodeName
, segmentCount
supporting given
* supportedCommandTypes
.
*
* @param nodeName The name of the node
* @param segmentCount The number of segments the node should have on the hash ring
* @param supportedCommandTypes The commands supported by this node
*/
public Member(String nodeName, int segmentCount, Set supportedCommandTypes) {
this.nodeName = nodeName;
this.supportedCommandTypes = Collections.unmodifiableSet(new HashSet(supportedCommandTypes));
Set newHashes = new TreeSet();
for (int t = 0; t < segmentCount; t++) {
String hash = Digester.md5Hex(nodeName + " #" + t);
newHashes.add(hash);
}
this.hashes = Collections.unmodifiableSet(newHashes);
}
/**
* Returns the name of this member. Members are typically uniquely identified by their name.
*
* Note that a single member may be presented by multiple {@code Member} instances if the number of segments
* differs per supported command type. Therefore, the name should not be considered an absolutely unique value.
*
* @return the name of this member
*/
public String name() {
return nodeName;
}
/**
* Returns the set of commands supported by this member.
*
* @return the set of commands supported by this member
*/
public Set supportedCommands() {
return supportedCommandTypes;
}
/**
* Returns the number of segments this member has on the consistent hash ring. Depending on the spread of the
* hashing algorithm used (default MD5), this number is an indication of the load of this node compared to
* other nodes.
*
* @return the number of segments this member has on the consistent hash ring
*/
public int segmentCount() {
return hashes.size();
}
/**
* Returns the hash values assigned to this member. These values are used to locate the member to handle any
* given command.
*
* @return the hash values assigned to this member
*/
public Set hashes() {
return hashes;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Member that = (Member) o;
if (!hashes.equals(that.hashes)) {
return false;
}
if (!nodeName.equals(that.nodeName)) {
return false;
}
if (!supportedCommandTypes.equals(that.supportedCommandTypes)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return nodeName.hashCode();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy