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

org.apache.kafka.raft.internals.VoterSet Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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 org.apache.kafka.raft.internals;

import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.kafka.common.Node;
import org.apache.kafka.common.Uuid;
import org.apache.kafka.common.feature.SupportedVersionRange;
import org.apache.kafka.common.message.VotersRecord;
import org.apache.kafka.common.network.ListenerName;
import org.apache.kafka.common.utils.Utils;

/**
 * A type for representing the set of voters for a topic partition.
 *
 * It encapsulates static information like a voter's endpoint and their supported kraft.version.
 *
 * It provides functionality for converting to and from {@code VotersRecord} and for converting
 * from the static configuration.
 */
final public class VoterSet {
    private final Map voters;

    VoterSet(Map voters) {
        if (voters.isEmpty()) {
            throw new IllegalArgumentException("Voters cannot be empty");
        }

        this.voters = voters;
    }

    /**
     * Returns the node information for all the given voter ids and listener.
     *
     * @param voterIds the ids of the voters
     * @param listenerName the name of the listener
     * @return the node information for all of the voter ids
     * @throws IllegalArgumentException if there are missing endpoints
     */
    public Set voterNodes(Stream voterIds, ListenerName listenerName) {
        return voterIds
            .map(voterId ->
                voterNode(voterId, listenerName).orElseThrow(() ->
                    new IllegalArgumentException(
                        String.format(
                            "Unable to find endpoint for voter %d and listener %s in %s",
                            voterId,
                            listenerName,
                            voters
                        )
                    )
                )
            )
            .collect(Collectors.toSet());
    }

    /**
     * Returns the node information for a given voter id and listener.
     *
     * @param voterId the id of the voter
     * @param listenerName the name of the listener
     * @return the node information if it exists, otherwise {@code Optional.empty()}
     */
    public Optional voterNode(int voterId, ListenerName listenerName) {
        return Optional.ofNullable(voters.get(voterId))
            .flatMap(voterNode -> voterNode.address(listenerName))
            .map(address -> new Node(voterId, address.getHostString(), address.getPort()));
    }

    /**
     * Returns if the node is a voter in the set of voters.
     *
     * If the voter set includes the directory id, the {@code nodeKey} directory id must match the
     * directory id specified by the voter set.
     *
     * If the voter set doesn't include the directory id ({@code Optional.empty()}), a node is in
     * the voter set as long as the node id matches. The directory id is not checked.
     *
     * @param nodeKey the node's id and directory id
     * @return true if the node is a voter in the voter set, otherwise false
     */
    public boolean isVoter(ReplicaKey nodeKey) {
        VoterNode node = voters.get(nodeKey.id());
        if (node != null) {
            if (node.voterKey().directoryId().isPresent()) {
                return node.voterKey().directoryId().equals(nodeKey.directoryId());
            } else {
                // configured voter set doesn't include a directory id so it is a voter as long as the node id
                // matches
                return true;
            }
        } else {
            return false;
        }
    }

    /**
     * Returns if the node is the only voter in the set of voters.
     *
     * @param nodeKey the node's id and directory id
     * @return true if the node is the only voter in the voter set, otherwise false
     */
    public boolean isOnlyVoter(ReplicaKey nodeKey) {
        return voters.size() == 1 && isVoter(nodeKey);
    }

    /**
     * Returns all of the voter ids.
     */
    public Set voterIds() {
        return voters.keySet();
    }

    /**
     * Adds a voter to the voter set.
     *
     * This object is immutable. A new voter set is returned if the voter was added.
     *
     * A new voter can be added to a voter set if its id doesn't already exist in the voter set.
     *
     * @param voter the new voter to add
     * @return a new voter set if the voter was added, otherwise {@code Optional.empty()}
     */
    public Optional addVoter(VoterNode voter) {
        if (voters.containsKey(voter.voterKey().id())) {
            return Optional.empty();
        }

        HashMap newVoters = new HashMap<>(voters);
        newVoters.put(voter.voterKey().id(), voter);

        return Optional.of(new VoterSet(newVoters));
    }

    /**
     * Remove a voter from the voter set.
     *
     * This object is immutable. A new voter set is returned if the voter was removed.
     *
     * A voter can be removed from the voter set if its id and directory id match.
     *
     * @param voterKey the voter key
     * @return a new voter set if the voter was removed, otherwise {@code Optional.empty()}
     */
    public Optional removeVoter(ReplicaKey voterKey) {
        VoterNode oldVoter = voters.get(voterKey.id());
        if (oldVoter != null && Objects.equals(oldVoter.voterKey(), voterKey)) {
            HashMap newVoters = new HashMap<>(voters);
            newVoters.remove(voterKey.id());

            return Optional.of(new VoterSet(newVoters));
        }

        return Optional.empty();
    }

    /**
     * Converts a voter set to a voters record for a given version.
     *
     * @param version the version of the voters record
     */
    public VotersRecord toVotersRecord(short version) {
        Function voterConvertor = voter -> {
            Iterator endpoints = voter
                .listeners()
                .entrySet()
                .stream()
                .map(entry ->
                    new VotersRecord.Endpoint()
                        .setName(entry.getKey().value())
                        .setHost(entry.getValue().getHostString())
                        .setPort(entry.getValue().getPort())
                )
                .iterator();

            VotersRecord.KRaftVersionFeature kraftVersionFeature = new VotersRecord.KRaftVersionFeature()
                .setMinSupportedVersion(voter.supportedKRaftVersion().min())
                .setMaxSupportedVersion(voter.supportedKRaftVersion().max());

            return new VotersRecord.Voter()
                .setVoterId(voter.voterKey().id())
                .setVoterDirectoryId(voter.voterKey().directoryId().orElse(Uuid.ZERO_UUID))
                .setEndpoints(new VotersRecord.EndpointCollection(endpoints))
                .setKRaftVersionFeature(kraftVersionFeature);
        };

        List voterRecordVoters = voters
            .values()
            .stream()
            .map(voterConvertor)
            .collect(Collectors.toList());

        return new VotersRecord()
            .setVersion(version)
            .setVoters(voterRecordVoters);
    }

    /**
     * Determines if two sets of voters have an overlapping majority.
     *
     * An overlapping majority means that for all majorities in {@code this} set of voters and for
     * all majority in {@code that} set of voters, they have at least one voter in common.
     *
     * If this function returns true, it means that if one of the set of voters commits an offset,
     * the other set of voters cannot commit a conflicting offset.
     *
     * @param that the other voter set to compare
     * @return true if they have an overlapping majority, false otherwise
     */
    public boolean hasOverlappingMajority(VoterSet that) {
        Set thisReplicaKeys = voters
            .values()
            .stream()
            .map(VoterNode::voterKey)
            .collect(Collectors.toSet());

        Set thatReplicaKeys = that.voters
            .values()
            .stream()
            .map(VoterNode::voterKey)
            .collect(Collectors.toSet());

        if (Utils.diff(HashSet::new, thisReplicaKeys, thatReplicaKeys).size() > 1) return false;
        if (Utils.diff(HashSet::new, thatReplicaKeys, thisReplicaKeys).size() > 1) return false;

        return true;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        VoterSet that = (VoterSet) o;

        return voters.equals(that.voters);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(voters);
    }

    @Override
    public String toString() {
        return String.format("VoterSet(voters=%s)", voters);
    }

    public final static class VoterNode {
        private final ReplicaKey voterKey;
        private final Map listeners;
        private final SupportedVersionRange supportedKRaftVersion;

        VoterNode(
            ReplicaKey voterKey,
            Map listeners,
            SupportedVersionRange supportedKRaftVersion
        ) {
            this.voterKey = voterKey;
            this.listeners = listeners;
            this.supportedKRaftVersion = supportedKRaftVersion;
        }

        public ReplicaKey voterKey() {
            return voterKey;
        }

        Map listeners() {
            return listeners;
        }

        SupportedVersionRange supportedKRaftVersion() {
            return supportedKRaftVersion;
        }


        Optional address(ListenerName listener) {
            return Optional.ofNullable(listeners.get(listener));
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            VoterNode that = (VoterNode) o;

            if (!Objects.equals(voterKey, that.voterKey)) return false;
            if (!Objects.equals(supportedKRaftVersion, that.supportedKRaftVersion)) return false;
            if (!Objects.equals(listeners, that.listeners)) return false;

            return true;
        }

        @Override
        public int hashCode() {
            return Objects.hash(voterKey, listeners, supportedKRaftVersion);
        }

        @Override
        public String toString() {
            return String.format(
                "VoterNode(voterKey=%s, listeners=%s, supportedKRaftVersion=%s)",
                voterKey,
                listeners,
                supportedKRaftVersion
            );
        }
    }

    /**
     * Converts a {@code VotersRecord} to a {@code VoterSet}.
     *
     * @param voters the set of voters control record
     * @return the voter set
     */
    public static VoterSet fromVotersRecord(VotersRecord voters) {
        HashMap voterNodes = new HashMap<>(voters.voters().size());
        for (VotersRecord.Voter voter: voters.voters()) {
            final Optional directoryId;
            if (!voter.voterDirectoryId().equals(Uuid.ZERO_UUID)) {
                directoryId = Optional.of(voter.voterDirectoryId());
            } else {
                directoryId = Optional.empty();
            }

            Map listeners = new HashMap<>(voter.endpoints().size());
            for (VotersRecord.Endpoint endpoint : voter.endpoints()) {
                listeners.put(
                    ListenerName.normalised(endpoint.name()),
                    InetSocketAddress.createUnresolved(endpoint.host(), endpoint.port())
                );
            }

            voterNodes.put(
                voter.voterId(),
                new VoterNode(
                    ReplicaKey.of(voter.voterId(), directoryId),
                    listeners,
                    new SupportedVersionRange(
                        voter.kRaftVersionFeature().minSupportedVersion(),
                        voter.kRaftVersionFeature().maxSupportedVersion()
                    )
                )
            );
        }

        return new VoterSet(voterNodes);
    }

    /**
     * Creates a voter set from a map of socket addresses.
     *
     * @param listener the listener name for all of the endpoints
     * @param voters the socket addresses by voter id
     * @return the voter set
     */
    public static VoterSet fromInetSocketAddresses(ListenerName listener, Map voters) {
        Map voterNodes = voters
            .entrySet()
            .stream()
            .collect(
                Collectors.toMap(
                    Map.Entry::getKey,
                    entry -> new VoterNode(
                        ReplicaKey.of(entry.getKey(), Optional.empty()),
                        Collections.singletonMap(listener, entry.getValue()),
                        new SupportedVersionRange((short) 0, (short) 0)
                    )
                )
            );

        return new VoterSet(voterNodes);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy