All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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