package org.apache.solr.cloud; /* * 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. */ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.solr.common.SolrException; import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.cloud.ZkCmdExecutor; import org.apache.solr.common.cloud.ZooKeeperException; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.KeeperException.ConnectionLossException; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.Watcher.Event.EventType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Leader Election process. This class contains the logic by which a * leader is chosen. First call * {@link #setup(ElectionContext)} to ensure * the election process is init'd. Next call * {@link #joinElection(ElectionContext, boolean)} to start the leader election. * * The implementation follows the classic ZooKeeper recipe of creating an * ephemeral, sequential node for each candidate and then looking at the set * of such nodes - if the created node is the lowest sequential node, the * candidate that created the node is the leader. If not, the candidate puts * a watch on the next lowest node it finds, and if that node goes down, * starts the whole process over by checking if it's the lowest sequential node, etc. * */ public class LeaderElector { private static Logger log = LoggerFactory.getLogger(LeaderElector.class); static final String ELECTION_NODE = "/election"; private final static Pattern LEADER_SEQ = Pattern.compile(".*?/?.*?-n_(\\d+)"); private final static Pattern SESSION_ID = Pattern.compile(".*?/?(.*?-.*?)-n_\\d+"); private final static Pattern NODE_NAME = Pattern.compile(".*?/?(.*?-)(.*?)-n_\\d+"); protected SolrZkClient zkClient; private ZkCmdExecutor zkCmdExecutor; private volatile ElectionContext context; public LeaderElector(SolrZkClient zkClient) { this.zkClient = zkClient; zkCmdExecutor = new ZkCmdExecutor(zkClient.getZkClientTimeout()); } public ElectionContext getContext() { return context; } /** * Check if the candidate with the given n_* sequence number is the leader. * If it is, set the leaderId on the leader zk node. If it is not, start * watching the candidate that is in line before this one - if it goes down, check * if this candidate is the leader again. * * @param replacement has someone else been the leader already? */ private void checkIfIamLeader(final int seq, final ElectionContext context, boolean replacement) throws KeeperException, InterruptedException, IOException { context.checkIfIamLeaderFired(); // get all other numbers... final String holdElectionPath = context.electionPath + ELECTION_NODE; List<String> seqs = zkClient.getChildren(holdElectionPath, null, true); sortSeqs(seqs); List<Integer> intSeqs = getSeqs(seqs); if (intSeqs.size() == 0) { log.warn("Our node is no longer in line to be leader"); return; } if (seq <= intSeqs.get(0)) { // first we delete the node advertising the old leader in case the ephem is still there try { zkClient.delete(context.leaderPath, -1, true); } catch(Exception e) { // fine } runIamLeaderProcess(context, replacement); } else { // I am not the leader - watch the node below me int i = 1; for (; i < intSeqs.size(); i++) { int s = intSeqs.get(i); if (seq < s) { // we found who we come before - watch the guy in front break; } } int index = i - 2; if (index < 0) { log.warn("Our node is no longer in line to be leader"); return; } try { zkClient.getData(holdElectionPath + "/" + seqs.get(index), new Watcher() { @Override public void process(WatchedEvent event) { // session events are not change events, // and do not remove the watcher if (EventType.None.equals(event.getType())) { return; } // am I the next leader? try { checkIfIamLeader(seq, context, true); } catch (InterruptedException e) { // Restore the interrupted status Thread.currentThread().interrupt(); log.warn("", e); } catch (IOException e) { log.warn("", e); } catch (Exception e) { log.warn("", e); } } }, null, true); } catch (KeeperException.SessionExpiredException e) { throw e; } catch (KeeperException e) { log.warn("Failed setting watch", e); // we couldn't set our watch - the node before us may already be down? // we need to check if we are the leader again checkIfIamLeader(seq, context, true); } } } // TODO: get this core param out of here protected void runIamLeaderProcess(final ElectionContext context, boolean weAreReplacement) throws KeeperException, InterruptedException, IOException { context.runLeaderProcess(weAreReplacement,0); } /** * Returns int given String of form n_0000000001 or n_0000000003, etc. * * @return sequence number */ public static int getSeq(String nStringSequence) { int seq = 0; Matcher m = LEADER_SEQ.matcher(nStringSequence); if (m.matches()) { seq = Integer.parseInt(m.group(1)); } else { throw new IllegalStateException("Could not find regex match in:" + nStringSequence); } return seq; } private String getNodeId(String nStringSequence) { String id; Matcher m = SESSION_ID.matcher(nStringSequence); if (m.matches()) { id = m.group(1); } else { throw new IllegalStateException("Could not find regex match in:" + nStringSequence); } return id; } public static String getNodeName(String nStringSequence){ String result; Matcher m = NODE_NAME.matcher(nStringSequence); if (m.matches()) { result = m.group(2); } else { throw new IllegalStateException("Could not find regex match in:" + nStringSequence); } return result; } /** * Returns int list given list of form n_0000000001, n_0000000003, etc. * * @return int seqs */ private List<Integer> getSeqs(List<String> seqs) { List<Integer> intSeqs = new ArrayList<>(seqs.size()); for (String seq : seqs) { intSeqs.add(getSeq(seq)); } return intSeqs; } /** * Begin participating in the election process. Gets a new sequential number * and begins watching the node with the sequence number before it, unless it * is the lowest number, in which case, initiates the leader process. If the * node that is watched goes down, check if we are the new lowest node, else * watch the next lowest numbered node. * * @return sequential node number */ public int joinElection(ElectionContext context, boolean replacement) throws KeeperException, InterruptedException, IOException { context.joinedElectionFired(); final String shardsElectZkPath = context.electionPath + LeaderElector.ELECTION_NODE; long sessionId = zkClient.getSolrZooKeeper().getSessionId(); String id = sessionId + "-" + context.id; String leaderSeqPath = null; boolean cont = true; int tries = 0; while (cont) { try { leaderSeqPath = zkClient.create(shardsElectZkPath + "/" + id + "-n_", null, CreateMode.EPHEMERAL_SEQUENTIAL, false); context.leaderSeqPath = leaderSeqPath; cont = false; } catch (ConnectionLossException e) { // we don't know if we made our node or not... List<String> entries = zkClient.getChildren(shardsElectZkPath, null, true); boolean foundId = false; for (String entry : entries) { String nodeId = getNodeId(entry); if (id.equals(nodeId)) { // we did create our node... foundId = true; break; } } if (!foundId) { cont = true; if (tries++ > 20) { throw new ZooKeeperException(SolrException.ErrorCode.SERVER_ERROR, "", e); } try { Thread.sleep(50); } catch (InterruptedException e2) { Thread.currentThread().interrupt(); } } } catch (KeeperException.NoNodeException e) { // we must have failed in creating the election node - someone else must // be working on it, lets try again if (tries++ > 20) { throw new ZooKeeperException(SolrException.ErrorCode.SERVER_ERROR, "", e); } cont = true; try { Thread.sleep(50); } catch (InterruptedException e2) { Thread.currentThread().interrupt(); } } } int seq = getSeq(leaderSeqPath); checkIfIamLeader(seq, context, replacement); return seq; } /** * Set up any ZooKeeper nodes needed for leader election. */ public void setup(final ElectionContext context) throws InterruptedException, KeeperException { this.context = context; String electZKPath = context.electionPath + LeaderElector.ELECTION_NODE; zkCmdExecutor.ensureExists(electZKPath, zkClient); } /** * Sort n string sequence list. */ public static void sortSeqs(List<String> seqs) { Collections.sort(seqs, new Comparator<String>() { @Override public int compare(String o1, String o2) { return Integer.valueOf(getSeq(o1)).compareTo( Integer.valueOf(getSeq(o2))); } }); } void retryElection() throws KeeperException, InterruptedException, IOException { context.cancelElection(); joinElection(context, true); } }