package org.infinispan.distribution.ch.impl;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;
import org.infinispan.commons.hash.Hash;
import org.infinispan.commons.marshall.InstanceReusingAdvancedExternalizer;
import org.infinispan.commons.util.Immutables;
import org.infinispan.commons.util.Util;
import org.infinispan.distribution.ch.ConsistentHash;
import org.infinispan.globalstate.ScopedPersistentState;
import org.infinispan.marshall.core.Ids;
import org.infinispan.remoting.transport.Address;
import org.infinispan.topology.PersistentUUID;
import org.infinispan.commons.util.SmallIntSet;
import net.jcip.annotations.Immutable;
/**
* Default {@link ConsistentHash} implementation. This object is immutable.
*
* @author Dan Berindei
* @author anistor@redhat.com
* @since 5.2
*/
@Immutable
public class DefaultConsistentHash implements ConsistentHash {
// State constants
private static final String STATE_CAPACITY_FACTOR = "capacityFactor.%d";
private static final String STATE_CAPACITY_FACTORS = "capacityFactors";
private static final String STATE_NUM_OWNERS = "numOwners";
private static final String STATE_NUM_SEGMENTS = "numSegments";
private static final String STATE_SEGMENT_OWNER = "segmentOwner.%d.%d";
private static final String STATE_SEGMENT_OWNERS = "segmentOwners";
private static final String STATE_SEGMENT_OWNER_COUNT = "segmentOwner.%d.num";
private final Hash hashFunction;
private final int numOwners;
/**
* The membership of the cache topology that uses this CH.
*/
private final List<Address> members;
private final float[] capacityFactors;
/**
* The routing table.
*/
private final List<Address>[] segmentOwners;
private final int segmentSize;
public DefaultConsistentHash(Hash hashFunction, int numOwners, int numSegments, List<Address> members,
Map<Address, Float> capacityFactors, List<Address>[] segmentOwners) {
if (numSegments < 1)
throw new IllegalArgumentException("The number of segments must be strictly positive");
if (numOwners < 1)
throw new IllegalArgumentException("The number of owners must be strictly positive");
this.numOwners = numOwners;
this.hashFunction = hashFunction;
this.members = new ArrayList<>(members);
if (capacityFactors == null) {
this.capacityFactors = null;
} else {
this.capacityFactors = new float[members.size()];
for (int i = 0; i < this.capacityFactors.length; i++) {
this.capacityFactors[i] = capacityFactors.get(members.get(i));
}
}
this.segmentOwners = new List[numSegments];
for (int i = 0; i < numSegments; i++) {
if (segmentOwners[i] == null || segmentOwners[i].isEmpty()) {
throw new IllegalArgumentException("Segment owner list cannot be null or empty");
}
this.segmentOwners[i] = Immutables.immutableListCopy(segmentOwners[i]);
}
this.segmentSize = Util.getSegmentSize(numSegments);
}
// Only used by the externalizer, so we can skip copying collections
private DefaultConsistentHash(Hash hashFunction, int numOwners, int numSegments, List<Address> members,
float[] capacityFactors, List<Address>[] segmentOwners) {
if (numSegments < 1)
throw new IllegalArgumentException("The number of segments must be strictly positive");
if (numOwners < 1)
throw new IllegalArgumentException("The number of owners must be strictly positive");
this.numOwners = numOwners;
this.hashFunction = hashFunction;
this.members = members;
this.capacityFactors = capacityFactors;
for (int i = 0; i < numSegments; i++) {
if (segmentOwners[i] == null || segmentOwners[i].size() == 0) {
throw new IllegalArgumentException("Segment owner list cannot be null or empty");
}
}
this.segmentOwners = segmentOwners;
this.segmentSize = Util.getSegmentSize(numSegments);
}
DefaultConsistentHash(ScopedPersistentState state) {
this.numOwners = Integer.parseInt(state.getProperty(STATE_NUM_OWNERS));
int numMembers = Integer.parseInt(state.getProperty(ConsistentHashPersistenceConstants.STATE_MEMBERS));
this.members = new ArrayList<>(numMembers);
for(int i = 0; i < numMembers; i++) {
PersistentUUID uuid = PersistentUUID.fromString(state.getProperty(String.format(ConsistentHashPersistenceConstants.STATE_MEMBER, i)));
this.members.add(uuid);
}
int numSegments = state.getIntProperty(STATE_NUM_SEGMENTS);
this.segmentSize = Util.getSegmentSize(numSegments);
this.segmentOwners = new List[numSegments];
for (int i = 0; i < segmentOwners.length; i++) {
int segmentOwnerCount = Integer.parseInt(state.getProperty(String.format(STATE_SEGMENT_OWNER_COUNT, i)));
segmentOwners[i] = new ArrayList<>();
for (int j = 0; j < segmentOwnerCount; j++) {
PersistentUUID uuid = PersistentUUID.fromString(state.getProperty(String.format(STATE_SEGMENT_OWNER, i, j)));
segmentOwners[i].add(uuid);
}
}
int numCapacityFactors = Integer.parseInt(state.getProperty(STATE_CAPACITY_FACTORS));
this.capacityFactors = new float[numCapacityFactors];
for (int i = 0; i < numCapacityFactors; i++) {
this.capacityFactors[i] = Float.parseFloat(state.getProperty(String.format(STATE_CAPACITY_FACTOR, i)));
}
this.hashFunction = Util.getInstance(state.getProperty(ConsistentHashPersistenceConstants.STATE_HASH_FUNCTION), null);
}
@Override
public Hash getHashFunction() {
return hashFunction;
}
@Override
public int getNumSegments() {
return segmentOwners.length;
}
@Override
public Set<Integer> getSegmentsForOwner(Address owner) {
if (owner == null) {
throw new IllegalArgumentException("owner cannot be null");
}
if (!members.contains(owner)) {
throw new IllegalArgumentException("Node " + owner + " is not a member");
}
Set<Integer> segments = new SmallIntSet(segmentOwners.length);
for (int segment = 0; segment < segmentOwners.length; segment++) {
if (segmentOwners[segment].contains(owner)) {
segments.add(segment);
}
}
return segments;
}
@Override
public Set<Integer> getPrimarySegmentsForOwner(Address owner) {
if (owner == null) {
throw new IllegalArgumentException("owner cannot be null");
}
if (!members.contains(owner)) {
throw new IllegalArgumentException("Node " + owner + " is not a member");
}
SmallIntSet segments = new SmallIntSet(segmentOwners.length);
for (int segment = 0; segment < segmentOwners.length; segment++) {
if (owner.equals(segmentOwners[segment].get(0))) {
segments.add(segment);
}
}
return segments;
}
@Override
public int getSegment(Object key) {
// The result must always be positive, so we make sure the dividend is positive first
return getNormalizedHash(key) / segmentSize;
}
private int getNormalizedHash(Object key) {
return hashFunction.hash(key) & Integer.MAX_VALUE;
}
/**
* @deprecated Since 8.2, use {@link HashFunctionPartitioner#getSegmentEndHashes()} instead.
*/
@Deprecated
public List<Integer> getSegmentEndHashes() {
int numSegments = segmentOwners.length;
List<Integer> hashes = new ArrayList<>(numSegments);
for (int i = 0; i < numSegments; i++) {
hashes.add(((i + 1) % numSegments) * segmentSize);
}
return hashes;
}
@Override
public List<Address> locateOwnersForSegment(int segmentId) {
return segmentOwners[segmentId];
}
@Override
public Address locatePrimaryOwnerForSegment(int segmentId) {
return segmentOwners[segmentId].get(0);
}
@Override
public List<Address> getMembers() {
return members;
}
@Override
public int getNumOwners() {
return numOwners;
}
@Override
public boolean isSegmentLocalToNode(Address nodeAddress, int segmentId) {
return segmentOwners[segmentId].contains(nodeAddress);
}
@Override
public int hashCode() {
int result = numOwners;
result = 31 * result + members.hashCode();
result = 31 * result + Arrays.hashCode(segmentOwners);
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DefaultConsistentHash that = (DefaultConsistentHash) o;
if (numOwners != that.numOwners) return false;
if (segmentOwners.length != that.segmentOwners.length) return false;
if (!hashFunction.equals(that.hashFunction)) return false;
if (!members.equals(that.members)) return false;
for (int i = 0; i < segmentOwners.length; i++) {
if (!segmentOwners[i].equals(that.segmentOwners[i]))
return false;
}
return true;
}
@Override
public String toString() {
OwnershipStatistics stats = new OwnershipStatistics(this, members);
StringBuilder sb = new StringBuilder("DefaultConsistentHash{");
sb.append("ns=").append(segmentOwners.length);
sb.append(", owners = (").append(members.size()).append(")[");
boolean first = true;
for (Address a : members) {
if (first) {
first = false;
} else {
sb.append(", ");
}
int primaryOwned = stats.getPrimaryOwned(a);
int owned = stats.getOwned(a);
sb.append(a).append(": ").append(primaryOwned).append('+').append(owned - primaryOwned);
}
sb.append("]}");
return sb.toString();
}
@Override
public String getRoutingTableAsString() {
StringBuilder sb = new StringBuilder();
for (Address a : members) {
if (sb.length() > 0) {
sb.append("\n ");
}
Set<Integer> primarySegments = getPrimarySegmentsForOwner(a);
sb.append(a).append(" primary: ").append(primarySegments);
Set<Integer> backupSegments = getSegmentsForOwner(a);
backupSegments.removeAll(primarySegments);
sb.append(", backup: ").append(backupSegments);
}
return sb.toString();
}
/**
* Merges two consistent hash objects that have the same number of segments, numOwners and hash function.
* For each segment, the primary owner of the first CH has priority, the other primary owners become backups.
*/
public DefaultConsistentHash union(DefaultConsistentHash dch2) {
if (!hashFunction.equals(dch2.getHashFunction())) {
throw new IllegalArgumentException("The consistent hash objects must have the same hash function");
}
int numSegments = getNumSegments();
if (numSegments != dch2.getNumSegments()) {
throw new IllegalArgumentException("The consistent hash objects must have the same number of segments");
}
if (numOwners != dch2.getNumOwners()) {
throw new IllegalArgumentException("The consistent hash objects must have the same number of owners");
}
List<Address> unionMembers = new ArrayList<>(this.members);
mergeLists(unionMembers, dch2.getMembers());
List<Address>[] unionSegmentOwners = new List[numSegments];
for (int i = 0; i < numSegments; i++) {
unionSegmentOwners[i] = new ArrayList<>(locateOwnersForSegment(i));
mergeLists(unionSegmentOwners[i], dch2.locateOwnersForSegment(i));
}
Map<Address, Float> unionCapacityFactors = null;
if (this.capacityFactors != null || dch2.capacityFactors != null) {
unionCapacityFactors = new HashMap<>();
if (this.capacityFactors != null) {
unionCapacityFactors.putAll(this.getCapacityFactors());
} else {
for (Address node : this.members) {
unionCapacityFactors.put(node, 1.0f);
}
}
if (dch2.capacityFactors != null) {
unionCapacityFactors.putAll(dch2.getCapacityFactors());
} else {
for (Address node : dch2.members) {
unionCapacityFactors.put(node, 1.0f);
}
}
}
return new DefaultConsistentHash(hashFunction, numOwners, numSegments, unionMembers, unionCapacityFactors, unionSegmentOwners);
}
/**
* Adds all elements from <code>src</code> list that do not already exist in <code>dest</code> list to the latter.
*
* @param dest List where elements are added
* @param src List of elements to add - this is never modified
*/
private void mergeLists(List<Address> dest, List<Address> src) {
for (Address node : src) {
if (!dest.contains(node)) {
dest.add(node);
}
}
}
public Map<Address, Float> getCapacityFactors() {
if (capacityFactors == null)
return null;
Map<Address, Float> capacityFactorsMap = new HashMap<>(members.size());
for (int i = 0; i < members.size(); i++) {
capacityFactorsMap.put(members.get(i), capacityFactors[i]);
}
return capacityFactorsMap;
}
public String prettyPrintOwnership() {
StringBuilder sb = new StringBuilder();
for (Address member : getMembers()) {
sb.append("\n").append(member).append(":");
for (int segment = 0; segment < segmentOwners.length; segment++) {
int index = segmentOwners[segment].indexOf(member);
if (index >= 0) {
sb.append(' ').append(segment);
if (index == 0) {
sb.append('\'');
}
}
}
}
return sb.toString();
}
@Override
public void toScopedState(ScopedPersistentState state) {
state.setProperty(ConsistentHashPersistenceConstants.STATE_CONSISTENT_HASH, this.getClass().getName());
state.setProperty(STATE_NUM_OWNERS, numOwners);
state.setProperty(STATE_NUM_SEGMENTS, getNumSegments());
state.setProperty(ConsistentHashPersistenceConstants.STATE_MEMBERS, members.size());
for (int i = 0; i < members.size(); i++) {
state.setProperty(String.format(ConsistentHashPersistenceConstants.STATE_MEMBER, i),
members.get(i).toString());
}
state.setProperty(STATE_CAPACITY_FACTORS, capacityFactors.length);
for (int i = 0; i < capacityFactors.length; i++) {
state.setProperty(String.format(STATE_CAPACITY_FACTOR, i), capacityFactors[i]);
}
state.setProperty(STATE_SEGMENT_OWNERS, segmentOwners.length);
for (int i = 0; i < segmentOwners.length; i++) {
List<Address> segmentOwnerAddresses = segmentOwners[i];
state.setProperty(String.format(STATE_SEGMENT_OWNER_COUNT, i), segmentOwnerAddresses.size());
for(int j = 0; j < segmentOwnerAddresses.size(); j++) {
state.setProperty(String.format(STATE_SEGMENT_OWNER, i, j),
segmentOwnerAddresses.get(j).toString());
}
}
state.setProperty(ConsistentHashPersistenceConstants.STATE_HASH_FUNCTION, hashFunction.getClass().getName());
}
@Override
public ConsistentHash remapAddresses(UnaryOperator<Address> remapper) {
List<Address> remappedMembers = new ArrayList<>(members.size());
for (Address member : members) {
Address a = remapper.apply(member);
if (a == null) {
return null;
}
remappedMembers.add(a);
}
Map<Address, Float> remappedCapacityFactors = null;
if (capacityFactors != null) {
remappedCapacityFactors = new HashMap<>(members.size());
for(int i=0; i < members.size(); i++) {
remappedCapacityFactors.put(remapper.apply(members.get(i)), capacityFactors[i]);
}
}
List<Address>[] remappedSegmentOwners = new List[segmentOwners.length];
for(int i=0; i < segmentOwners.length; i++) {
List<Address> remappedOwners = new ArrayList<>(segmentOwners[i].size());
for (Address address : segmentOwners[i]) {
remappedOwners.add(remapper.apply(address));
}
remappedSegmentOwners[i] = remappedOwners;
}
return new DefaultConsistentHash(this.hashFunction, this.numOwners, this.segmentOwners.length, remappedMembers,
remappedCapacityFactors, remappedSegmentOwners);
}
public static class Externalizer extends InstanceReusingAdvancedExternalizer<DefaultConsistentHash> {
@Override
public void doWriteObject(ObjectOutput output, DefaultConsistentHash ch) throws IOException {
output.writeInt(ch.segmentOwners.length);
output.writeInt(ch.numOwners);
output.writeObject(ch.members);
output.writeObject(ch.capacityFactors);
output.writeObject(ch.hashFunction);
// Avoid computing the identityHashCode for every ImmutableListCopy/Address
HashMap<Address, Integer> memberIndexes = getMemberIndexMap(ch.members);
for (int i = 0; i < ch.segmentOwners.length; i++) {
List<Address> owners = ch.segmentOwners[i];
output.writeInt(owners.size());
for (Address owner : owners) {
output.writeInt(memberIndexes.get(owner));
}
}
}
@Override
@SuppressWarnings("unchecked")
public DefaultConsistentHash doReadObject(ObjectInput unmarshaller) throws IOException, ClassNotFoundException {
int numSegments = unmarshaller.readInt();
int numOwners = unmarshaller.readInt();
List<Address> members = (List<Address>) unmarshaller.readObject();
float[] capacityFactors = (float[]) unmarshaller.readObject();
Hash hash = (Hash) unmarshaller.readObject();
List<Address>[] segmentOwners = new List[numSegments];
for (int i = 0; i < numSegments; i++) {
int size = unmarshaller.readInt();
Address[] owners = new Address[size];
for (int j = 0; j < size; j++) {
int ownerIndex = unmarshaller.readInt();
owners[j] = members.get(ownerIndex);
}
segmentOwners[i] = Immutables.immutableListWrap(owners);
}
return new DefaultConsistentHash(hash, numOwners, numSegments, members, capacityFactors, segmentOwners);
}
private HashMap<Address, Integer> getMemberIndexMap(List<Address> members) {
HashMap<Address, Integer> memberIndexes = new HashMap<>(members.size());
for (int i = 0; i < members.size(); i++) {
memberIndexes.put(members.get(i), i);
}
return memberIndexes;
}
@Override
public Integer getId() {
return Ids.DEFAULT_CONSISTENT_HASH;
}
@Override
public Set<Class<? extends DefaultConsistentHash>> getTypeClasses() {
return Collections.singleton(DefaultConsistentHash.class);
}
}
}