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

io.aeron.cluster.ClusterMember Maven / Gradle / Ivy

There is a newer version: 1.46.2
Show newest version
/*
 * Copyright 2014-2023 Real Logic Limited.
 *
 * Licensed 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
 *
 * https://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 io.aeron.cluster;

import io.aeron.Aeron;
import io.aeron.ChannelUri;
import io.aeron.ExclusivePublication;
import io.aeron.Publication;
import io.aeron.cluster.client.ClusterEvent;
import io.aeron.cluster.client.ClusterException;
import io.aeron.exceptions.RegistrationException;
import org.agrona.CloseHelper;
import org.agrona.ErrorHandler;
import org.agrona.collections.ArrayUtil;
import org.agrona.collections.Int2ObjectHashMap;

import java.util.List;

import static io.aeron.Aeron.NULL_VALUE;
import static io.aeron.CommonContext.ENDPOINT_PARAM_NAME;
import static io.aeron.archive.client.AeronArchive.NULL_POSITION;

/**
 * Represents a member of the cluster that participates in consensus for storing state from the perspective
 * of any single member. It is not a global view of the cluster, perspectives only exist from a vantage point.
 */
public final class ClusterMember
{
    static final ClusterMember[] EMPTY_MEMBERS = new ClusterMember[0];

    private boolean isBallotSent;
    private boolean isLeader;
    private boolean hasRequestedJoin;
    private boolean hasTerminated;
    private int id;
    private long leadershipTermId = Aeron.NULL_VALUE;
    private long candidateTermId = Aeron.NULL_VALUE;
    private long catchupReplaySessionId = Aeron.NULL_VALUE;
    private long catchupReplayCorrelationId = Aeron.NULL_VALUE;
    private long changeCorrelationId = Aeron.NULL_VALUE;
    private long removalPosition = NULL_POSITION;
    private long logPosition = NULL_POSITION;
    private long timeOfLastAppendPositionNs = Aeron.NULL_VALUE;
    private ExclusivePublication publication;
    private String consensusChannel;
    private final String consensusEndpoint;
    private final String ingressEndpoint;
    private final String logEndpoint;
    private final String catchupEndpoint;
    private final String archiveEndpoint;
    private final String endpoints;
    private Boolean vote = null;

    /**
     * Construct a new member of the cluster.
     *
     * @param id                unique id for the member.
     * @param ingressEndpoint   address and port endpoint to which cluster clients send ingress.
     * @param consensusEndpoint address and port endpoint to which other cluster members connect.
     * @param logEndpoint       address and port endpoint to which the log is replicated.
     * @param catchupEndpoint   address and port endpoint to which a stream is replayed for catchup to the leader.
     * @param archiveEndpoint   address and port endpoint to which the archive control channel can be reached.
     * @param endpoints         comma separated list of endpoints.
     */
    public ClusterMember(
        final int id,
        final String ingressEndpoint,
        final String consensusEndpoint,
        final String logEndpoint,
        final String catchupEndpoint,
        final String archiveEndpoint,
        final String endpoints)
    {
        this.id = id;
        this.ingressEndpoint = ingressEndpoint;
        this.consensusEndpoint = consensusEndpoint;
        this.logEndpoint = logEndpoint;
        this.catchupEndpoint = catchupEndpoint;
        this.archiveEndpoint = archiveEndpoint;
        this.endpoints = endpoints;
    }

    /**
     * Reset the state of a cluster member, so it can be canvassed and reestablished.
     */
    public void reset()
    {
        isBallotSent = false;
        isLeader = false;
        hasRequestedJoin = false;
        hasTerminated = false;
        vote = null;
        candidateTermId = Aeron.NULL_VALUE;
        leadershipTermId = Aeron.NULL_VALUE;
        logPosition = NULL_POSITION;
    }

    /**
     * Set if this member should be leader.
     *
     * @param isLeader value.
     * @return this for a fluent API.
     */
    public ClusterMember isLeader(final boolean isLeader)
    {
        this.isLeader = isLeader;
        return this;
    }

    /**
     * Is this member currently the leader?
     *
     * @return true if this member is currently the leader otherwise false.
     */
    public boolean isLeader()
    {
        return isLeader;
    }

    /**
     * Is the ballot for the current election sent to this member?
     *
     * @param isBallotSent is the ballot for the current election sent to this member?
     * @return this for a fluent API.
     */
    public ClusterMember isBallotSent(final boolean isBallotSent)
    {
        this.isBallotSent = isBallotSent;
        return this;
    }

    /**
     * Is the ballot for the current election sent to this member?
     *
     * @return true if the ballot has been sent for this member in the current election.
     */
    public boolean isBallotSent()
    {
        return isBallotSent;
    }

    /**
     * Set if this member requested to join the cluster.
     *
     * @param hasRequestedJoin the cluster.
     * @return this for a fluent API.
     */
    public ClusterMember hasRequestedJoin(final boolean hasRequestedJoin)
    {
        this.hasRequestedJoin = hasRequestedJoin;
        return this;
    }

    /**
     * Has this member requested to join the cluster?
     *
     * @return has this member requested to join the cluster?
     */
    public boolean hasRequestedJoin()
    {
        return hasRequestedJoin;
    }

    /**
     * Set if this member has terminated.
     *
     * @param hasTerminated in notification to the leader.
     * @return this for a fluent API.
     */
    public ClusterMember hasTerminated(final boolean hasTerminated)
    {
        this.hasTerminated = hasTerminated;
        return this;
    }

    /**
     * Has this member notified that it has terminated?
     *
     * @return has this member notified that it has terminated?
     */
    public boolean hasTerminated()
    {
        return hasTerminated;
    }

    /**
     * Set the log position as of appending the event to be removed from the cluster.
     *
     * @param removalPosition as of appending the event to be removed from the cluster.
     * @return this for a fluent API.
     */
    public ClusterMember removalPosition(final long removalPosition)
    {
        this.removalPosition = removalPosition;
        return this;
    }

    /**
     * The log position as of appending the event to be removed from the cluster.
     *
     * @return the log position as of appending the event to be removed from the cluster,
     * or {@link io.aeron.archive.client.AeronArchive#NULL_POSITION} if not requested remove.
     */
    public long removalPosition()
    {
        return removalPosition;
    }

    /**
     * Has this member requested to be removed from the cluster.
     *
     * @return true if this member requested to be removed from the cluster, otherwise false.
     */
    public boolean hasRequestedRemove()
    {
        return removalPosition != NULL_POSITION;
    }

    /**
     * Set the unique id for this member of the cluster.
     *
     * @param id for this member of the cluster.
     * @return this for a fluent API.
     */
    public ClusterMember id(final int id)
    {
        this.id = id;
        return this;
    }

    /**
     * Unique identity for this member in the cluster.
     *
     * @return the unique identity for this member in the cluster.
     */
    public int id()
    {
        return id;
    }

    /**
     * Set the result of the vote for this member. {@link Boolean#TRUE} means they voted for this member,
     * {@link Boolean#FALSE} means they voted against this member, and null means no vote was received.
     *
     * @param vote for this member in the election.
     * @return this for a fluent API.
     */
    public ClusterMember vote(final Boolean vote)
    {
        this.vote = vote;
        return this;
    }

    /**
     * The status of the vote for this member in an election. {@link Boolean#TRUE} means they voted for this member,
     * {@link Boolean#FALSE} means they voted against this member, and null means no vote was received.
     *
     * @return the status of the vote for this member in an election.
     */
    public Boolean vote()
    {
        return vote;
    }

    /**
     * The leadership term reached for the cluster member.
     *
     * @param leadershipTermId leadership term reached for the cluster member.
     * @return this for a fluent API.
     */
    public ClusterMember leadershipTermId(final long leadershipTermId)
    {
        this.leadershipTermId = leadershipTermId;
        return this;
    }

    /**
     * The leadership term reached for the cluster member.
     *
     * @return The leadership term reached for the cluster member.
     */
    public long leadershipTermId()
    {
        return leadershipTermId;
    }

    /**
     * The log position this member has persisted.
     *
     * @param logPosition this member has persisted.
     * @return this for a fluent API.
     */
    public ClusterMember logPosition(final long logPosition)
    {
        this.logPosition = logPosition;
        return this;
    }

    /**
     * The log position this member has persisted.
     *
     * @return the log position this member has persisted.
     */
    public long logPosition()
    {
        return logPosition;
    }

    /**
     * The candidate term id used when voting.
     *
     * @param candidateTermId used when voting.
     * @return this for a fluent API.
     */
    public ClusterMember candidateTermId(final long candidateTermId)
    {
        this.candidateTermId = candidateTermId;
        return this;
    }

    /**
     * The candidate term id used when voting.
     *
     * @return the candidate term id used when voting.
     */
    public long candidateTermId()
    {
        return candidateTermId;
    }

    /**
     * The session id for the replay when catching up to the leader.
     *
     * @param replaySessionId for the replay when catching up to the leader.
     * @return this for a fluent API.
     */
    public ClusterMember catchupReplaySessionId(final long replaySessionId)
    {
        this.catchupReplaySessionId = replaySessionId;
        return this;
    }

    /**
     * The session id for the replay when catching up to the leader.
     *
     * @return the session id for the replay when catching up to the leader.
     */
    public long catchupReplaySessionId()
    {
        return catchupReplaySessionId;
    }

    /**
     * The correlation id for the replay when catching up to the leader.
     *
     * @param correlationId for the replay when catching up to the leader.
     * @return this for a fluent API.
     */
    public ClusterMember catchupReplayCorrelationId(final long correlationId)
    {
        this.catchupReplayCorrelationId = correlationId;
        return this;
    }

    /**
     * The correlation id for the replay when catching up to the leader.
     *
     * @return the correlation id for the replay when catching up to the leader.
     */
    public long catchupReplayCorrelationId()
    {
        return catchupReplayCorrelationId;
    }

    /**
     * Correlation id assigned to the current action undertaken by the cluster member.
     *
     * @param correlationId assigned to the current action undertaken by the cluster member.
     * @return this for a fluent API.
     */
    public ClusterMember correlationId(final long correlationId)
    {
        this.changeCorrelationId = correlationId;
        return this;
    }

    /**
     * Correlation id assigned to the current action undertaken by the cluster member.
     *
     * @return correlation id assigned to the current action undertaken by the cluster member.
     */
    public long correlationId()
    {
        return changeCorrelationId;
    }

    /**
     * Time (in ns) of last received appendPosition.
     *
     * @param timeNs of the last received appendPosition.
     * @return this for a fluent API.
     */
    public ClusterMember timeOfLastAppendPositionNs(final long timeNs)
    {
        this.timeOfLastAppendPositionNs = timeNs;
        return this;
    }

    /**
     * Time (in ns) of last received appendPosition.
     *
     * @return time (in ns) of last received appendPosition or {@link Aeron#NULL_VALUE} if none received.
     */
    public long timeOfLastAppendPositionNs()
    {
        return timeOfLastAppendPositionNs;
    }

    /**
     * The address:port endpoint for this cluster member that other members connect to for achieving consensus.
     *
     * @return the address:port endpoint for this cluster member that other members will connect to for consensus.
     */
    public String consensusEndpoint()
    {
        return consensusEndpoint;
    }

    /**
     * The address:port endpoint for this cluster member that clients send ingress to.
     *
     * @return the address:port endpoint for this cluster member that listens for ingress.
     */
    public String ingressEndpoint()
    {
        return ingressEndpoint;
    }

    /**
     * The address:port endpoint for this cluster member that the log is replicated to.
     *
     * @return the address:port endpoint for this cluster member that the log is replicated to.
     */
    public String logEndpoint()
    {
        return logEndpoint;
    }

    /**
     * The address:port endpoint for this cluster member to which a stream is replayed for catchup to the leader.
     * 

* It is recommended a port of 0 is used, so it is system allocated to avoid potential clashes. * * @return the address:port endpoint for this cluster member to which a stream is replayed for catchup to the * leader. */ public String catchupEndpoint() { return catchupEndpoint; } /** * The address:port endpoint for this cluster member that the archive can be reached. * * @return the address:port endpoint for this cluster member that the archive can be reached. */ public String archiveEndpoint() { return archiveEndpoint; } /** * The string of endpoints for this member in a comma separated list in the same order they are parsed. * * @return list of endpoints for this member in a comma separated list. * @see #parse(String) */ public String endpoints() { return endpoints; } /** * The {@link Publication} used for send status updates to the member. * * @return {@link Publication} used for send status updates to the member. */ public ExclusivePublication publication() { return publication; } /** * {@link Publication} used for send status updates to the member. * * @param publication used for send status updates to the member. */ public void publication(final ExclusivePublication publication) { this.publication = publication; } /** * Close consensus publication and null out reference. * * @param errorHandler to capture errors during close. */ public void closePublication(final ErrorHandler errorHandler) { CloseHelper.close(errorHandler, publication); publication = null; } /** * Parse the details for a cluster members from a string. *

* * member-id,ingress:port,consensus:port,log:port,catchup:port,archive:port|1,... * * * @param value of the string to be parsed. * @return An array of cluster members. */ public static ClusterMember[] parse(final String value) { if (null == value || value.length() == 0) { return ClusterMember.EMPTY_MEMBERS; } final String[] memberValues = value.split("\\|"); final int length = memberValues.length; final ClusterMember[] members = new ClusterMember[length]; for (int i = 0; i < length; i++) { final String idAndEndpoints = memberValues[i]; final String[] memberAttributes = idAndEndpoints.split(","); final int clusterMemberId; if (memberAttributes.length != 6) { throw new ClusterException("invalid member value: " + idAndEndpoints + " within: " + value); } try { clusterMemberId = Integer.parseInt(memberAttributes[0]); } catch (final NumberFormatException ex) { throw new ClusterException("invalid cluster member id, must be an integer value", ex); } final String endpoints = String.join( ",", memberAttributes[1], memberAttributes[2], memberAttributes[3], memberAttributes[4], memberAttributes[5]); members[i] = new ClusterMember( clusterMemberId, memberAttributes[1], memberAttributes[2], memberAttributes[3], memberAttributes[4], memberAttributes[5], endpoints); } return members; } /** * Parse a string containing the endpoints for a cluster node and passing to * {@link #ClusterMember(int, String, String, String, String, String, String)}. * * @param id of the member node. * @param endpoints comma separated. * @return the {@link ClusterMember} with the endpoints set. */ public static ClusterMember parseEndpoints(final int id, final String endpoints) { final String[] memberAttributes = endpoints.split(","); if (memberAttributes.length != 5) { throw new ClusterException("invalid member value: " + endpoints); } return new ClusterMember( id, memberAttributes[0], memberAttributes[1], memberAttributes[2], memberAttributes[3], memberAttributes[4], endpoints); } /** * Encode member endpoints from a cluster members array to a String. * * @param clusterMembers to fill the details from. * @return String representation suitable for use with {@link #parse(String)}. */ public static String encodeAsString(final ClusterMember[] clusterMembers) { if (0 == clusterMembers.length) { return ""; } final StringBuilder builder = new StringBuilder(); for (int i = 0, length = clusterMembers.length; i < length; i++) { final ClusterMember member = clusterMembers[i]; builder .append(member.id) .append(',') .append(member.endpoints); if ((length - 1) != i) { builder.append('|'); } } return builder.toString(); } /** * Encode member endpoints from a cluster members {@link List} to a String. * * @param clusterMembers to fill the details from. * @return String representation suitable for use with {@link #parse(String)}. */ public static String encodeAsString(final List clusterMembers) { if (0 == clusterMembers.size()) { return ""; } final StringBuilder builder = new StringBuilder(); for (int i = 0, length = clusterMembers.size(); i < length; i++) { final ClusterMember member = clusterMembers.get(i); builder .append(member.id) .append(',') .append(member.endpoints); if ((length - 1) != i) { builder.append('|'); } } return builder.toString(); } /** * Copy votes from one array of members to another where the {@link #id()}s match. * * @param srcMembers to copy the votes from. * @param dstMembers to copy the votes to. */ public static void copyVotes(final ClusterMember[] srcMembers, final ClusterMember[] dstMembers) { for (final ClusterMember srcMember : srcMembers) { final ClusterMember dstMember = findMember(dstMembers, srcMember.id); if (null != dstMember) { dstMember.vote = srcMember.vote; } } } /** * Add the publications for sending consensus messages to the other members of the cluster. * * @param members of the cluster. * @param exclude this member when adding publications. * @param channelTemplate for the publications. * @param streamId for the publications. * @param aeron to add the publications to. * @param errorHandler to log registration exceptions to. */ public static void addConsensusPublications( final ClusterMember[] members, final ClusterMember exclude, final String channelTemplate, final int streamId, final Aeron aeron, final ErrorHandler errorHandler) { final ChannelUri channelUri = ChannelUri.parse(channelTemplate); for (final ClusterMember member : members) { if (member.id != exclude.id) { channelUri.put(ENDPOINT_PARAM_NAME, member.consensusEndpoint); member.consensusChannel = channelUri.toString(); tryAddPublication(member, streamId, aeron, errorHandler); } } } /** * Add an exclusive {@link Publication} for communicating to a member on the consensus channel. * * @param member to which the publication is addressed. * @param channelTemplate for the target member. * @param streamId for the target member. * @param aeron from which the publication will be created. * @param errorHandler to log registration exceptions to. */ public static void addConsensusPublication( final ClusterMember member, final String channelTemplate, final int streamId, final Aeron aeron, final ErrorHandler errorHandler) { if (null == member.consensusChannel) { final ChannelUri channelUri = ChannelUri.parse(channelTemplate); channelUri.put(ENDPOINT_PARAM_NAME, member.consensusEndpoint); member.consensusChannel = channelUri.toString(); } tryAddPublication(member, streamId, aeron, errorHandler); } /** * Try and add an exclusive {@link Publication} for communicating to a member on the consensus channel. * * @param member to which the publication is added. * @param streamId for the target member. * @param aeron from which the publication will be created. * @param errorHandler to log registration exceptions to. */ public static void tryAddPublication( final ClusterMember member, final int streamId, final Aeron aeron, final ErrorHandler errorHandler) { try { member.publication = aeron.addExclusivePublication(member.consensusChannel, streamId); } catch (final RegistrationException ex) { errorHandler.onError(new ClusterEvent( "failed to add consensus publication for member: " + member.id + " - " + ex.getMessage())); } } /** * Close the publications associated with members of the cluster used for the consensus protocol. * * @param errorHandler to capture errors during close. * @param clusterMembers to close the publications for. */ public static void closeConsensusPublications(final ErrorHandler errorHandler, final ClusterMember[] clusterMembers) { for (final ClusterMember member : clusterMembers) { member.closePublication(errorHandler); } } /** * Populate map of {@link ClusterMember}s which can be looked up by id. * * @param clusterMembers to populate the map. * @param clusterMemberByIdMap to be populated. */ public static void addClusterMemberIds( final ClusterMember[] clusterMembers, final Int2ObjectHashMap clusterMemberByIdMap) { for (final ClusterMember member : clusterMembers) { clusterMemberByIdMap.put(member.id, member); } } /** * Check if the cluster leader has an active quorum of cluster followers. * * @param clusterMembers for the current cluster. * @param nowNs for the current time. * @param timeoutNs after which a follower is not considered active. * @return true if quorum of cluster members are considered active. */ public static boolean hasActiveQuorum( final ClusterMember[] clusterMembers, final long nowNs, final long timeoutNs) { int threshold = quorumThreshold(clusterMembers.length); for (final ClusterMember member : clusterMembers) { if (member.isLeader || nowNs <= (member.timeOfLastAppendPositionNs + timeoutNs)) { if (--threshold <= 0) { return true; } } } return false; } /** * The threshold of clusters members required to achieve quorum given a count of cluster members. * * @param memberCount for the cluster * @return the threshold for achieving quorum. */ public static int quorumThreshold(final int memberCount) { return (memberCount >> 1) + 1; } /** * Calculate the position reached by a quorum of cluster members. * * @param members of the cluster. * @param rankedPositions temp array to be used for sorting the positions to avoid allocation. * @return the position reached by a quorum of cluster members. */ public static long quorumPosition(final ClusterMember[] members, final long[] rankedPositions) { final int length = rankedPositions.length; for (int i = 0; i < length; i++) { rankedPositions[i] = 0; } for (final ClusterMember member : members) { long newPosition = member.logPosition; for (int i = 0; i < length; i++) { final long rankedPosition = rankedPositions[i]; if (newPosition > rankedPosition) { rankedPositions[i] = newPosition; newPosition = rankedPosition; } } } return rankedPositions[length - 1]; } /** * Reset the log position of all the members to the provided value. * * @param clusterMembers to be reset. * @param logPosition to set for them all. */ public static void resetLogPositions(final ClusterMember[] clusterMembers, final long logPosition) { for (final ClusterMember member : clusterMembers) { member.logPosition = logPosition; } } /** * Has the voting members of a cluster arrived at provided position in their log. * * @param clusterMembers to check. * @param position to compare the {@link #logPosition()} against. * @param leadershipTermId expected of the members. * @return true if all members have reached this position otherwise false. */ public static boolean hasVotersAtPosition( final ClusterMember[] clusterMembers, final long position, final long leadershipTermId) { for (final ClusterMember member : clusterMembers) { if (member.vote != null && (member.logPosition < position || member.leadershipTermId != leadershipTermId)) { return false; } } return true; } /** * Has a quorum of members of appended a position to their local log. * * @param clusterMembers to check. * @param position to compare the {@link #logPosition()} against. * @param leadershipTermId expected of the members. * @return true if a quorum of members reached this position otherwise false. */ public static boolean hasQuorumAtPosition( final ClusterMember[] clusterMembers, final long position, final long leadershipTermId) { int votes = 0; for (final ClusterMember member : clusterMembers) { if (member.leadershipTermId == leadershipTermId && member.logPosition >= position) { ++votes; } } return votes >= ClusterMember.quorumThreshold(clusterMembers.length); } /** * Reset the state of all cluster members. * * @param members to reset. */ public static void reset(final ClusterMember[] members) { for (final ClusterMember member : members) { member.reset(); } } /** * Become a candidate by voting for yourself and resetting the other votes to {@link Aeron#NULL_VALUE}. * * @param members to reset the votes for. * @param candidateTermId for the candidacy. * @param candidateMemberId for the election. */ public static void becomeCandidate( final ClusterMember[] members, final long candidateTermId, final int candidateMemberId) { for (final ClusterMember member : members) { if (member.id == candidateMemberId) { member.vote(Boolean.TRUE) .candidateTermId(candidateTermId) .isBallotSent(true); } else { member.vote(null) .candidateTermId(Aeron.NULL_VALUE) .isBallotSent(false); } } } /** * Is a member considered unanimously to be leader after voting. *

* If a leader has been gracefully closed then it is not included in the membership for considering a unanimous * position but will be considered in the membership for quorum. * * @param clusterMembers to check for votes. * @param candidateTermId for the vote. * @param gracefulClosedLeaderId id of a leader if gracefully closed otherwise {@link Aeron#NULL_VALUE}. * @return {@code true} if all members voted positively. */ public static boolean isUnanimousLeader( final ClusterMember[] clusterMembers, final long candidateTermId, final int gracefulClosedLeaderId) { int votes = 0; for (final ClusterMember member : clusterMembers) { if (member.id == gracefulClosedLeaderId) { continue; } if (candidateTermId != member.candidateTermId || !Boolean.TRUE.equals(member.vote)) { return false; } votes++; } return votes >= ClusterMember.quorumThreshold(clusterMembers.length); } /** * Is this member considered leader by a quorum of members by having positive votes being counted for a majority * and no negative votes. * * @param clusterMembers to check for votes. * @param candidateTermId for the vote. * @return {@code true} if sufficient positive votes being counted for a majority and no negative votes. */ public static boolean isQuorumLeader(final ClusterMember[] clusterMembers, final long candidateTermId) { int votes = 0; for (final ClusterMember member : clusterMembers) { if (candidateTermId == member.candidateTermId) { if (Boolean.FALSE.equals(member.vote)) { return false; } if (Boolean.TRUE.equals(member.vote)) { ++votes; } } } return votes >= ClusterMember.quorumThreshold(clusterMembers.length); } /** * Determine which member of a cluster this is and check endpoints. * * @param clusterMembers for the current cluster which can be null. * @param memberId for this member. * @param memberEndpoints for this member. * @return the {@link ClusterMember} determined. */ public static ClusterMember determineMember( final ClusterMember[] clusterMembers, final int memberId, final String memberEndpoints) { ClusterMember member = NULL_VALUE != memberId ? ClusterMember.findMember(clusterMembers, memberId) : null; if ((null == clusterMembers || 0 == clusterMembers.length) && null == member) { member = ClusterMember.parseEndpoints(NULL_VALUE, memberEndpoints); } else { if (null == member) { throw new ClusterException("memberId=" + memberId + " not found in clusterMembers"); } if (!memberEndpoints.isEmpty()) { ClusterMember.validateMemberEndpoints(member, memberEndpoints); } } return member; } /** * Check the member with the memberEndpoints. * * @param member to check memberEndpoints against. * @param memberEndpoints to check member against. * @see ConsensusModule.Context#memberEndpoints() * @see ConsensusModule.Context#clusterMembers() */ public static void validateMemberEndpoints(final ClusterMember member, final String memberEndpoints) { final ClusterMember endpoints = ClusterMember.parseEndpoints(Aeron.NULL_VALUE, memberEndpoints); if (!areSameEndpoints(member, endpoints)) { throw new ClusterException( "clusterMembers and endpoints differ: " + member.endpoints + " != " + memberEndpoints); } } /** * Are two cluster members using the same endpoints? * * @param lhs to compare for equality. * @param rhs to compare for equality. * @return true if both are using the same endpoints or false if not. */ public static boolean areSameEndpoints(final ClusterMember lhs, final ClusterMember rhs) { return lhs.ingressEndpoint.equals(rhs.ingressEndpoint) && lhs.consensusEndpoint.equals(rhs.consensusEndpoint) && lhs.logEndpoint.equals(rhs.logEndpoint) && lhs.catchupEndpoint.equals(rhs.catchupEndpoint) && lhs.archiveEndpoint.equals(rhs.archiveEndpoint); } /** * Is the member considered a candidate by a unanimous view to be a suitable candidate in an election. *

* If a leader has been gracefully closed then it is not included in the membership for considering a unanimous * position but will be considered in the membership for quorum. * * @param clusterMembers to compare the candidate against. * @param candidate for leadership. * @param gracefulClosedLeaderId id of a leader if gracefully closed otherwise {@link Aeron#NULL_VALUE}. * @return true if the candidate is suitable otherwise false. */ public static boolean isUnanimousCandidate( final ClusterMember[] clusterMembers, final ClusterMember candidate, final int gracefulClosedLeaderId) { int possibleVotes = 0; for (final ClusterMember member : clusterMembers) { if (member.id == gracefulClosedLeaderId) { continue; } if (NULL_POSITION == member.logPosition || compareLog(candidate, member) < 0) { return false; } possibleVotes++; } return possibleVotes >= ClusterMember.quorumThreshold(clusterMembers.length); } /** * Has the member achieved a quorum view to be a suitable candidate in an election. * * @param clusterMembers to compare the candidate against. * @param candidate for leadership. * @return true if the candidate is suitable otherwise false. */ public static boolean isQuorumCandidate(final ClusterMember[] clusterMembers, final ClusterMember candidate) { int possibleVotes = 0; for (final ClusterMember member : clusterMembers) { if (NULL_POSITION == member.logPosition || compareLog(candidate, member) < 0) { continue; } ++possibleVotes; } return possibleVotes >= ClusterMember.quorumThreshold(clusterMembers.length); } /** * The result is positive if lhs has the more recent log, zero if logs are equal, and negative if rhs has the more * recent log. * * @param lhsLogLeadershipTermId term for which the position is most recent. * @param lhsLogPosition reached in the provided term. * @param rhsLogLeadershipTermId term for which the position is most recent. * @param rhsLogPosition reached in the provided term. * @return positive if lhs has the more recent log, zero if logs are equal, and negative if rhs has the more * recent log. */ public static int compareLog( final long lhsLogLeadershipTermId, final long lhsLogPosition, final long rhsLogLeadershipTermId, final long rhsLogPosition) { if (lhsLogLeadershipTermId > rhsLogLeadershipTermId) { return 1; } else if (lhsLogLeadershipTermId < rhsLogLeadershipTermId) { return -1; } else if (lhsLogPosition > rhsLogPosition) { return 1; } else if (lhsLogPosition < rhsLogPosition) { return -1; } return 0; } /** * The result is positive if lhs has the more recent log, zero if logs are equal, and negative if rhs has the more * recent log. * * @param lhs member to compare. * @param rhs member to compare. * @return positive if lhs has the more recent log, zero if logs are equal, and negative if rhs has the more * recent log. */ public static int compareLog(final ClusterMember lhs, final ClusterMember rhs) { return compareLog(lhs.leadershipTermId, lhs.logPosition, rhs.leadershipTermId, rhs.logPosition); } /** * Is the string of member endpoints not duplicated in the members. * * @param members to check if the provided endpoints have a duplicate. * @param endpoints to check for duplicates. * @return true if no duplicate is found otherwise false. */ public static boolean notDuplicateEndpoint(final ClusterMember[] members, final String endpoints) { for (final ClusterMember member : members) { if (member.endpoints.equals(endpoints)) { return false; } } return true; } /** * Find the index at which a member id is present. * * @param clusterMembers to be searched. * @param memberId to search for. * @return the index at which the member id is found otherwise {@link ArrayUtil#UNKNOWN_INDEX}. */ public static int findMemberIndex(final ClusterMember[] clusterMembers, final int memberId) { final int length = clusterMembers.length; int index = ArrayUtil.UNKNOWN_INDEX; for (int i = 0; i < length; i++) { if (memberId == clusterMembers[i].id) { index = i; } } return index; } /** * Find a {@link ClusterMember} with a given id. * * @param clusterMembers to search. * @param memberId to search for. * @return the {@link ClusterMember} if found otherwise null. */ public static ClusterMember findMember(final ClusterMember[] clusterMembers, final int memberId) { for (final ClusterMember member : clusterMembers) { if (memberId == member.id) { return member; } } return null; } /** * Add a new member to an array of {@link ClusterMember}s. * * @param oldMembers to add to. * @param newMember to add. * @return a new array containing the old members plus the new member. */ public static ClusterMember[] addMember(final ClusterMember[] oldMembers, final ClusterMember newMember) { return ArrayUtil.add(oldMembers, newMember); } /** * Remove a member from an array if found, otherwise return the array unmodified. * * @param oldMembers to remove a member from. * @param memberId of the member to remove. * @return a new array with the member removed or the existing array if not found. */ public static ClusterMember[] removeMember(final ClusterMember[] oldMembers, final int memberId) { final int memberIndex = findMemberIndex(oldMembers, memberId); if (ArrayUtil.UNKNOWN_INDEX != memberIndex && 1 == oldMembers.length) { return EMPTY_MEMBERS; } else { return ArrayUtil.remove(oldMembers, memberIndex); } } /** * Find the highest member id in an array of members. * * @param clusterMembers to search for the highest id. * @return the highest id otherwise {@link Aeron#NULL_VALUE} if empty. */ public static int highMemberId(final ClusterMember[] clusterMembers) { int highId = Aeron.NULL_VALUE; for (final ClusterMember member : clusterMembers) { highId = Math.max(highId, member.id); } return highId; } /** * Create a string of ingress endpoints by member id in format {@code id=endpoint,id=endpoint, ...}. * * @param members for which the ingress endpoints string will be generated. * @return a string of ingress endpoints by id. */ public static String ingressEndpoints(final ClusterMember[] members) { final StringBuilder builder = new StringBuilder(100); for (int i = 0, length = members.length; i < length; i++) { if (0 != i) { builder.append(','); } final ClusterMember member = members[i]; builder.append(member.id).append('=').append(member.ingressEndpoint); } return builder.toString(); } /** * Run through the list of cluster members and set the isLeader field based on the supplied leaderMemberId. * * @param clusterMembers list of cluster members. * @param leaderMemberId memberId of the current leader. */ public static void setIsLeader(final ClusterMember[] clusterMembers, final int leaderMemberId) { for (final ClusterMember clusterMember : clusterMembers) { clusterMember.isLeader(clusterMember.id() == leaderMemberId); } } /** * {@inheritDoc} */ public String toString() { return "ClusterMember{" + "id=" + id + ", isBallotSent=" + isBallotSent + ", isLeader=" + isLeader + ", hasRequestedJoin=" + hasRequestedJoin + ", leadershipTermId=" + leadershipTermId + ", logPosition=" + logPosition + ", candidateTermId=" + candidateTermId + ", catchupReplaySessionId=" + catchupReplaySessionId + ", correlationId=" + changeCorrelationId + ", removalPosition=" + removalPosition + ", timeOfLastAppendPositionNs=" + timeOfLastAppendPositionNs + ", ingressEndpoint='" + ingressEndpoint + '\'' + ", consensusEndpoint='" + consensusEndpoint + '\'' + ", logEndpoint='" + logEndpoint + '\'' + ", catchupEndpoint='" + catchupEndpoint + '\'' + ", archiveEndpoint='" + archiveEndpoint + '\'' + ", endpoints='" + endpoints + '\'' + ", publication=" + publication + ", vote=" + vote + '}'; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy