package org.infinispan.distribution;
import static org.infinispan.distribution.DistributionTestHelper.addressOf;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.infinispan.Cache;
import org.infinispan.distribution.ch.ConsistentHash;
import org.infinispan.marshall.core.ExternalPojo;
import org.infinispan.remoting.transport.Address;
/**
* A special type of key that if passed a cache in its constructor, will ensure it will always be assigned to that cache
* (plus however many additional caches in the hash space).
*
* Note that this only works if all the caches have joined a single cluster before creating the key.
* If the cluster membership changes then the keys may move to other servers.
*/
public class MagicKey implements Serializable, ExternalPojo {
/**
* The serialVersionUID
*/
private static final long serialVersionUID = -835275755945753954L;
private static final WeakHashMap<Integer, int[]> hashCodes = new WeakHashMap<>();
private static final AtomicLong counter = new AtomicLong();
/**
* The name is used only for easier debugging and may be null. It is not part of equals()/hashCode().
*/
private final String name;
private final int hashcode;
/**
* As hash codes can collide, using counter makes the key unique.
*/
private final long unique;
private final int segment;
private final String address;
public MagicKey(String name, Cache<?, ?> primaryOwner) {
this.name = name;
Address primaryAddress = addressOf(primaryOwner);
this.address = primaryAddress.toString();
LocalizedCacheTopology cacheTopology = primaryOwner.getAdvancedCache().getDistributionManager().getCacheTopology();
ConsistentHash ch = cacheTopology.getWriteConsistentHash();
int segment = findSegment(ch.getNumSegments(), s -> primaryAddress.equals(ch.locatePrimaryOwnerForSegment(s)));
if (segment < 0) {
throw new IllegalStateException("Could not find any segment owned by " + primaryOwner +
", primary segments: " + segments(primaryOwner));
}
this.segment = segment;
hashcode = getHashCodeForSegment(cacheTopology, segment);
unique = counter.getAndIncrement();
}
public MagicKey(String name, Cache<?, ?> primaryOwner, Cache<?, ?>... backupOwners) {
this.name = name;
Address primaryAddress = addressOf(primaryOwner);
this.address = primaryAddress.toString();
LocalizedCacheTopology cacheTopology = primaryOwner.getAdvancedCache().getDistributionManager().getCacheTopology();
ConsistentHash ch = cacheTopology.getWriteConsistentHash();
segment = findSegment(ch.getNumSegments(), s -> {
List<Address> owners = ch.locateOwnersForSegment(s);
if (!primaryAddress.equals(owners.get(0))) return false;
for (Cache<?, ?> backup : backupOwners) {
if (!owners.contains(addressOf(backup))) return false;
}
return true;
});
if (segment < 0) {
throw new IllegalStateException("Could not find any segment owned by " + primaryOwner + ", "
+ Arrays.toString(backupOwners) + ", primary segments: " + segments(primaryOwner)
+ ", backup segments: " + Stream.of(backupOwners).collect(Collectors.toMap(Function.identity(), this::segments)));
}
hashcode = getHashCodeForSegment(cacheTopology, segment);
unique = counter.getAndIncrement();
}
private int findSegment(int numSegments, Predicate<Integer> predicate) {
// use random offset so that we don't use only lower segments
int offset = ThreadLocalRandom.current().nextInt(numSegments);
for (int i = 0; i < numSegments; ++i) {
int segment = (offset + i) % numSegments;
if (predicate.test(segment)) {
return segment;
}
}
return -1;
}
private static synchronized int getHashCodeForSegment(LocalizedCacheTopology cacheTopology, int segment) {
int numSegments = cacheTopology.getReadConsistentHash().getNumSegments();
// Caching the hash codes prevents random failures in tests where we create many magic keys
int[] hcs = hashCodes.computeIfAbsent(numSegments, k -> new int[numSegments]);
int hc = hcs[segment];
if (hc != 0) {
return hc;
}
Random r = new Random();
int attemptsLeft = 100 * numSegments;
int dummy;
do {
dummy = r.nextInt();
attemptsLeft--;
if (attemptsLeft < 0) {
throw new IllegalStateException("Could not find any key in segment " + segment);
}
} while (cacheTopology.getSegment(dummy) != segment);
return hcs[segment] = dummy;
}
private Set<Integer> segments(Cache<?, ?> owner) {
return owner.getAdvancedCache().getDistributionManager().getWriteConsistentHash()
.getPrimarySegmentsForOwner(owner.getCacheManager().getAddress());
}
public MagicKey(Cache<?, ?> primaryOwner) {
this(null, primaryOwner);
}
public MagicKey(Cache<?, ?> primaryOwner, Cache<?, ?>... backupOwners) {
this(null, primaryOwner, backupOwners);
}
@Override
public int hashCode () {
return hashcode;
}
@Override
public boolean equals (Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MagicKey magicKey = (MagicKey) o;
return hashcode == magicKey.hashcode && address.equals(magicKey.address) &&
Objects.equals(name, magicKey.name) && unique == magicKey.unique;
}
@Override
public String toString() {
return String.format("MagicKey%s{%X/%08X/%d@%s}", name == null ? "" : "#" + name,
unique, hashcode, segment, address);
}
}