/* * 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.activemq.artemis.core.server.cluster.qourum; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import org.apache.activemq.artemis.api.core.Pair; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.api.core.client.ClusterTopologyListener; import org.apache.activemq.artemis.api.core.client.TopologyMember; import org.apache.activemq.artemis.core.client.impl.TopologyMemberImpl; import org.apache.activemq.artemis.core.server.ActiveMQComponent; import org.apache.activemq.artemis.core.server.cluster.ClusterControl; import org.apache.activemq.artemis.core.server.cluster.ClusterController; /** * A QourumManager can be used to register a {@link org.apache.activemq.artemis.core.server.cluster.qourum.Quorum} to receive notifications * about changes to the cluster. A {@link org.apache.activemq.artemis.core.server.cluster.qourum.Quorum} can then issue a vote to the * remaining nodes in a cluster for a specific outcome */ public final class QuorumManager implements ClusterTopologyListener, ActiveMQComponent { private final ExecutorService executor; private final ClusterController clusterController; /** * all the current registered {@link org.apache.activemq.artemis.core.server.cluster.qourum.Quorum}'s */ private final Map<String, Quorum> quorums = new HashMap<>(); /** * any currently running runnables. */ private final Map<QuorumVote, VoteRunnableHolder> voteRunnables = new HashMap<>(); private final Map<SimpleString, QuorumVoteHandler> handlers = new HashMap<>(); private boolean started = false; /** * this is the max size that the cluster has been. */ private int maxClusterSize = 0; public QuorumManager(ExecutorService threadPool, ClusterController clusterController) { this.clusterController = clusterController; this.executor = threadPool; } /** * we start by simply creating the server locator and connecting in a separate thread * * @throws Exception */ @Override public void start() throws Exception { if (started) return; started = true; } /** * stops the server locator * * @throws Exception */ @Override public void stop() throws Exception { if (!started) return; synchronized (voteRunnables) { started = false; for (VoteRunnableHolder voteRunnableHolder : voteRunnables.values()) { for (VoteRunnable runnable : voteRunnableHolder.runnables) { runnable.close(); } } } for (Quorum quorum : quorums.values()) { quorum.close(); } quorums.clear(); } /** * are we started * * @return */ @Override public boolean isStarted() { return started; } /** * registers a {@link org.apache.activemq.artemis.core.server.cluster.qourum.Quorum} so that it can be notified of changes in the cluster. * * @param quorum */ public void registerQuorum(Quorum quorum) { quorums.put(quorum.getName(), quorum); quorum.setQuorumManager(this); } /** * unregisters a {@link org.apache.activemq.artemis.core.server.cluster.qourum.Quorum}. * * @param quorum */ public void unRegisterQuorum(Quorum quorum) { quorums.remove(quorum.getName()); } /** * called by the {@link org.apache.activemq.artemis.core.client.impl.ServerLocatorInternal} when the topology changes. we update the * {@code maxClusterSize} if needed and inform the {@link org.apache.activemq.artemis.core.server.cluster.qourum.Quorum}'s. * * @param topologyMember the topolgy changed * @param last if the whole cluster topology is being transmitted (after adding the listener to * the cluster connection) this parameter will be {@code true} for the last topology */ @Override public void nodeUP(TopologyMember topologyMember, boolean last) { final int newClusterSize = clusterController.getDefaultClusterSize(); maxClusterSize = newClusterSize > maxClusterSize ? newClusterSize : maxClusterSize; for (Quorum quorum : quorums.values()) { quorum.nodeUp(clusterController.getDefaultClusterTopology()); } } /** * notify the {@link org.apache.activemq.artemis.core.server.cluster.qourum.Quorum} of a topology change. * * @param eventUID * @param nodeID the id of the node leaving the cluster */ @Override public void nodeDown(long eventUID, String nodeID) { for (Quorum quorum : quorums.values()) { quorum.nodeDown(clusterController.getDefaultClusterTopology(), eventUID, nodeID); } } /** * returns the maximum size this cluster has been. * * @return max size */ public int getMaxClusterSize() { return maxClusterSize; } /** * ask the quorum to vote within a specific quorum. * * @param quorumVote the vote to acquire */ public void vote(final QuorumVote quorumVote) { List<VoteRunnable> runnables = new ArrayList<>(); synchronized (voteRunnables) { if (!started) return; //send a vote to each node for (TopologyMemberImpl tm : clusterController.getDefaultClusterTopology().getMembers()) { //but not ourselves if (!tm.getNodeId().equals(clusterController.getNodeID().toString())) { Pair<TransportConfiguration, TransportConfiguration> pair = tm.getConnector(); final TransportConfiguration serverTC = pair.getA(); VoteRunnable voteRunnable = new VoteRunnable(serverTC, quorumVote); runnables.add(voteRunnable); } } if (runnables.size() > 0) { voteRunnables.put(quorumVote, new VoteRunnableHolder(quorumVote, runnables, runnables.size())); for (VoteRunnable runnable : runnables) { executor.submit(runnable); } } else { quorumVote.allVotesCast(clusterController.getDefaultClusterTopology()); } } } /** * handle a vote received on the quorum * * @param handler the name of the handler to use for the vote * @param vote the vote * @return the updated vote */ public Vote vote(SimpleString handler, Vote vote) { QuorumVoteHandler quorumVoteHandler = handlers.get(handler); return quorumVoteHandler.vote(vote); } /** * must be called by the quorum when it is happy on an outcome. only one vote can take place at anyone time for a * specific quorum * * @param quorumVote the vote */ public void voteComplete(QuorumVoteServerConnect quorumVote) { VoteRunnableHolder holder = voteRunnables.remove(quorumVote); if (holder != null) { for (VoteRunnable runnable : holder.runnables) { runnable.close(); } } } /** * called to register vote handlers on the quorum * * @param quorumVoteHandler the vote handler */ public void registerQuorumHandler(QuorumVoteHandler quorumVoteHandler) { handlers.put(quorumVoteHandler.getQuorumName(), quorumVoteHandler); } @Override public String toString() { return QuorumManager.class.getSimpleName() + "(server=" + clusterController.getIdentity() + ")"; } public QuorumVoteHandler getVoteHandler(SimpleString handler) { return handlers.get(handler); } private final class VoteRunnableHolder { private final QuorumVote quorumVote; private final List<VoteRunnable> runnables; private int size; private VoteRunnableHolder(QuorumVote quorumVote, List<VoteRunnable> runnables, int size) { this.quorumVote = quorumVote; this.runnables = runnables; this.size = size; } public synchronized void voteComplete() { size--; if (size <= 0) { quorumVote.allVotesCast(clusterController.getDefaultClusterTopology()); } } } /** * this will connect to a node and then cast a vote. whether or not this vote is asked of the target node is dependent * on {@link org.apache.activemq.artemis.core.server.cluster.qourum.Vote#isRequestServerVote()} */ private final class VoteRunnable implements Runnable { private final TransportConfiguration serverTC; private final QuorumVote quorumVote; private ClusterControl clusterControl; private VoteRunnable(TransportConfiguration serverTC, QuorumVote quorumVote) { this.serverTC = serverTC; this.quorumVote = quorumVote; } @Override public void run() { try { Vote vote; if (!started) return; //try to connect to the node i want to send a vote to clusterControl = clusterController.connectToNode(serverTC); clusterControl.authorize(); //if we are successful get the vote and check whether we need to send it to the target server, //just connecting may be enough vote = quorumVote.connected(); if (vote.isRequestServerVote()) { vote = clusterControl.sendQuorumVote(quorumVote.getName(), vote); quorumVote.vote(vote); } else { quorumVote.vote(vote); } } catch (Exception e) { Vote vote = quorumVote.notConnected(); quorumVote.vote(vote); } finally { try { if (clusterControl != null) { clusterControl.close(); } } catch (Exception e) { //ignore } QuorumManager.this.votingComplete(quorumVote); } } public void close() { if (clusterControl != null) { clusterControl.close(); } } } private void votingComplete(QuorumVote quorumVote) { VoteRunnableHolder voteRunnableHolder = voteRunnables.get(quorumVote); if (voteRunnableHolder != null) { voteRunnableHolder.voteComplete(); } } }