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); } } }