/*
* Copyright 2014 WANdisco
*
* WANdisco licenses this file to you 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 c5db.interfaces.replication;
import c5db.replication.generated.QuorumConfigurationMessage;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.collect.SortedMultiset;
import com.google.common.collect.TreeMultiset;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Immutable value type representing a configuration of which peers are members of a quorum.
* It satisfies the following invariants: if isTransitional is true, then allPeers is
* the union of prevPeers and nextPeers. If isTransitional is false, then prevPeers
* and nextPeers are empty.
*/
public final class QuorumConfiguration {
public final boolean isTransitional;
private final Set<Long> allPeers;
private final Set<Long> prevPeers;
private final Set<Long> nextPeers;
public static final QuorumConfiguration EMPTY = new QuorumConfiguration(new HashSet<>());
public static QuorumConfiguration of(Collection<Long> peerCollection) {
return new QuorumConfiguration(peerCollection);
}
public static QuorumConfiguration fromProtostuff(c5db.replication.generated.QuorumConfigurationMessage message) {
if (message.getTransitional()) {
return new QuorumConfiguration(message.getPrevPeersList(), message.getNextPeersList());
} else {
return new QuorumConfiguration(message.getAllPeersList());
}
}
public QuorumConfiguration getTransitionalConfiguration(Collection<Long> newPeerCollection) {
if (isTransitional) {
return new QuorumConfiguration(prevPeers, newPeerCollection);
} else {
return new QuorumConfiguration(allPeers, newPeerCollection);
}
}
public QuorumConfiguration getCompletedConfiguration() {
assert isTransitional;
return new QuorumConfiguration(nextPeers);
}
public QuorumConfigurationMessage toProtostuff() {
return new QuorumConfigurationMessage(
isTransitional,
Lists.newArrayList(allPeers),
Lists.newArrayList(prevPeers),
Lists.newArrayList(nextPeers));
}
public Set<Long> allPeers() {
return allPeers;
}
public Set<Long> prevPeers() {
return prevPeers;
}
public Set<Long> nextPeers() {
return nextPeers;
}
public boolean isEmpty() {
return allPeers.size() == 0
&& prevPeers.size() == 0
&& nextPeers.size() == 0;
}
/**
* Determine if the peers in sourceSet include a majority of the peers in this configuration.
*/
public boolean setContainsMajority(Set<Long> sourceSet) {
if (isTransitional) {
return setComprisesMajorityOfAnotherSet(sourceSet, prevPeers)
&& setComprisesMajorityOfAnotherSet(sourceSet, nextPeers);
} else {
return setComprisesMajorityOfAnotherSet(sourceSet, allPeers);
}
}
/**
* Given a map which tells the last acknowledged entry index for different peers, find the maximum
* index value which is less than or equal to a majority of this configuration's peers' indexes.
*/
public long calculateCommittedIndex(Map<Long, Long> peersLastAckedIndex) {
if (isTransitional) {
return Math.min(
getGreatestIndexCommittedByMajority(prevPeers, peersLastAckedIndex),
getGreatestIndexCommittedByMajority(nextPeers, peersLastAckedIndex));
} else {
return getGreatestIndexCommittedByMajority(allPeers, peersLastAckedIndex);
}
}
private QuorumConfiguration(Collection<Long> peers) {
this.isTransitional = false;
allPeers = ImmutableSet.copyOf(peers);
prevPeers = nextPeers = ImmutableSet.of();
}
private QuorumConfiguration(Collection<Long> prevPeers, Collection<Long> nextPeers) {
this.isTransitional = true;
this.prevPeers = ImmutableSet.copyOf(prevPeers);
this.nextPeers = ImmutableSet.copyOf(nextPeers);
this.allPeers = Sets.union(this.prevPeers, this.nextPeers).immutableCopy();
}
private static long getGreatestIndexCommittedByMajority(Set<Long> peers, Map<Long, Long> peersLastAckedIndex) {
SortedMultiset<Long> committedIndexes = TreeMultiset.create();
committedIndexes.addAll(peers.stream().map(peerId
-> peersLastAckedIndex.getOrDefault(peerId, 0L)).collect(Collectors.toList()));
return Iterables.get(committedIndexes.descendingMultiset(), calculateNumericalMajority(peers.size()) - 1);
}
private static <T> boolean setComprisesMajorityOfAnotherSet(Set<T> sourceSet, Set<T> destinationSet) {
return Sets.intersection(sourceSet, destinationSet).size() >= calculateNumericalMajority(destinationSet.size());
}
private static int calculateNumericalMajority(int setSize) {
return (setSize / 2) + 1;
}
@Override
public String toString() {
return "QuorumConfiguration{" +
"isTransitional=" + isTransitional +
", allPeers=" + allPeers +
", prevPeers=" + prevPeers +
", nextPeers=" + nextPeers +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
QuorumConfiguration that = (QuorumConfiguration) o;
return isTransitional == that.isTransitional
&& allPeers.equals(that.allPeers)
&& nextPeers.equals(that.nextPeers)
&& prevPeers.equals(that.prevPeers);
}
@Override
public int hashCode() {
int result = (isTransitional ? 1 : 0);
result = 31 * result + allPeers.hashCode();
result = 31 * result + prevPeers.hashCode();
result = 31 * result + nextPeers.hashCode();
return result;
}
}