/* * 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.nifi.cluster.coordination.flow; import static java.util.Objects.requireNonNull; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import org.apache.nifi.cluster.protocol.DataFlow; import org.apache.nifi.cluster.protocol.NodeIdentifier; import org.apache.nifi.controller.StandardFlowSynchronizer; import org.apache.nifi.fingerprint.FingerprintFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <p> * An implementation of {@link FlowElection} that waits until either a maximum amount of time has elapsed * or a maximum number of Data Flows have entered the running to be elected, and then elects the 'winner' * based on the number of 'votes' that a particular DataFlow has received. This implementation considers * two Flows with the same fingerprint to be the same Flow. If there is a tie in the number of votes for * a particular DataFlow, one will be chosen in a non-deterministic manner. If multiple DataFlows are * presented with the same fingerprint but different Flows (for instance, the position of a component has * changed), one of the Flows with that fingerprint will be chosen in a non-deterministic manner. * </p> */ public class PopularVoteFlowElection implements FlowElection { private static final Logger logger = LoggerFactory.getLogger(PopularVoteFlowElection.class); private final long maxWaitNanos; private final Integer maxNodes; private final FingerprintFactory fingerprintFactory; private volatile Long startNanos = null; private volatile DataFlow electedDataFlow = null; private final Map<String, FlowCandidate> candidateByFingerprint = new HashMap<>(); public PopularVoteFlowElection(final long maxWait, final TimeUnit maxWaitPeriod, final Integer maxNodes, final FingerprintFactory fingerprintFactory) { this.maxWaitNanos = maxWaitPeriod.toNanos(maxWait); if (maxWaitNanos < 1) { throw new IllegalArgumentException("Maximum wait time to elect Cluster Flow cannot be less than 1 nanosecond"); } this.maxNodes = maxNodes; if (maxNodes != null && maxNodes < 1) { throw new IllegalArgumentException("Maximum number of nodes to wait on before electing Cluster Flow cannot be less than 1"); } this.fingerprintFactory = requireNonNull(fingerprintFactory); } @Override public synchronized boolean isElectionComplete() { if (electedDataFlow != null) { return true; } if (startNanos == null) { return false; } final long nanosSinceStart = System.nanoTime() - startNanos; if (nanosSinceStart > maxWaitNanos) { final FlowCandidate elected = performElection(); logger.info("Election is complete because the maximum allowed time has elapsed. " + "The elected dataflow is held by the following nodes: {}", elected.getNodes()); return true; } else if (maxNodes != null) { final int numVotes = getVoteCount(); if (numVotes >= maxNodes) { final FlowCandidate elected = performElection(); logger.info("Election is complete because the required number of nodes ({}) have voted. " + "The elected dataflow is held by the following nodes: {}", maxNodes, elected.getNodes()); return true; } } return false; } @Override public boolean isVoteCounted(final NodeIdentifier nodeIdentifier) { return candidateByFingerprint.values().stream() .anyMatch(candidate -> candidate.getNodes().contains(nodeIdentifier)); } private synchronized int getVoteCount() { return candidateByFingerprint.values().stream().mapToInt(candidate -> candidate.getVotes()).sum(); } @Override public synchronized DataFlow castVote(final DataFlow candidate, final NodeIdentifier nodeId) { if (candidate == null || isElectionComplete()) { return getElectedDataFlow(); } final String fingerprint = fingerprint(candidate); final FlowCandidate flowCandidate = candidateByFingerprint.computeIfAbsent(fingerprint, key -> new FlowCandidate(candidate)); final boolean voteCast = flowCandidate.vote(nodeId); if (startNanos == null) { startNanos = System.nanoTime(); } if (voteCast) { logger.info("Vote cast by {}; this flow now has {} votes", nodeId, flowCandidate.getVotes()); } if (isElectionComplete()) { return getElectedDataFlow(); } return null; // no elected candidate so return null } private String fingerprint(final DataFlow dataFlow) { final String flowFingerprint = fingerprintFactory.createFingerprint(dataFlow.getFlow()); final String authFingerprint = dataFlow.getAuthorizerFingerprint() == null ? "" : new String(dataFlow.getAuthorizerFingerprint(), StandardCharsets.UTF_8); final String candidateFingerprint = flowFingerprint + authFingerprint; return candidateFingerprint; } @Override public DataFlow getElectedDataFlow() { return electedDataFlow; } private FlowCandidate performElection() { if (candidateByFingerprint.isEmpty()) { return null; } final List<FlowCandidate> nonEmptyCandidates = candidateByFingerprint.values().stream() .filter(candidate -> !candidate.isFlowEmpty()) .collect(Collectors.toList()); if (nonEmptyCandidates.isEmpty()) { // All flow candidates are empty flows. Just use one of them. final FlowCandidate electedCandidate = candidateByFingerprint.values().iterator().next(); this.electedDataFlow = electedCandidate.getDataFlow(); return electedCandidate; } final FlowCandidate elected; if (nonEmptyCandidates.size() == 1) { // Only one flow is non-empty. Use that one. elected = nonEmptyCandidates.iterator().next(); } else { // Choose the non-empty flow that got the most votes. elected = nonEmptyCandidates.stream() .max((candidate1, candidate2) -> Integer.compare(candidate1.getVotes(), candidate2.getVotes())) .get(); } this.electedDataFlow = elected.getDataFlow(); return elected; } @Override public synchronized String getStatusDescription() { if (startNanos == null) { return "No votes have yet been cast."; } final StringBuilder descriptionBuilder = new StringBuilder("Election will complete in "); final long nanosElapsed = System.nanoTime() - startNanos; final long nanosLeft = maxWaitNanos - nanosElapsed; final long secsLeft = TimeUnit.NANOSECONDS.toSeconds(nanosLeft); if (secsLeft < 1) { descriptionBuilder.append("less than 1 second"); } else { descriptionBuilder.append(secsLeft).append(" seconds"); } if (maxNodes != null) { final int votesNeeded = maxNodes.intValue() - getVoteCount(); descriptionBuilder.append(" or after ").append(votesNeeded).append(" more vote"); descriptionBuilder.append(votesNeeded == 1 ? " is " : "s are "); descriptionBuilder.append("cast, whichever occurs first."); } return descriptionBuilder.toString(); } private static class FlowCandidate { private final DataFlow dataFlow; private final AtomicInteger voteCount = new AtomicInteger(0); private final Set<NodeIdentifier> nodeIds = Collections.synchronizedSet(new HashSet<>()); public FlowCandidate(final DataFlow dataFlow) { this.dataFlow = dataFlow; } /** * Casts a vote for this candidate for the given node identifier, if a vote has not already * been cast for this node identifier * * @param nodeId the node id that is casting the vote * @return <code>true</code> if the vote was case, <code>false</code> if this node id has already cast its vote */ public boolean vote(final NodeIdentifier nodeId) { if (nodeIds.add(nodeId)) { voteCount.incrementAndGet(); return true; } return false; } public int getVotes() { return voteCount.get(); } public DataFlow getDataFlow() { return dataFlow; } public boolean isFlowEmpty() { return StandardFlowSynchronizer.isEmpty(dataFlow); } public Set<NodeIdentifier> getNodes() { return nodeIds; } } }