/** * 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.camel.component.zookeeper.policy; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.camel.CamelContext; import org.apache.camel.StatefulService; import org.apache.camel.impl.JavaUuidGenerator; import org.apache.camel.spi.UuidGenerator; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2; import org.apache.curator.framework.recipes.locks.Lease; import org.apache.curator.framework.state.ConnectionState; import org.apache.curator.framework.state.ConnectionStateListener; import org.apache.curator.retry.ExponentialBackoffRetry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <code>CuratorMultiMasterLeaderElection</code> uses the leader election capabilities of a * ZooKeeper cluster to control which nodes are enabled. It is typically used in * fail-over scenarios controlling identical instances of an application across * a cluster of Camel based servers. <p> The election is configured providing the number of instances that are required * to be active.. * <p> All instances of the election must also be configured with the same path on the ZooKeeper * cluster where the election will be carried out. It is good practice for this * to indicate the application e.g. <tt>/someapplication/someroute/</tt> note * that these nodes should exist before using the election. <p> See <a * href="http://hadoop.apache.org/zookeeper/docs/current/recipes.html#sc_leaderElection"> * for more on how Leader election</a> is archived with ZooKeeper. */ public class CuratorMultiMasterLeaderElection implements ConnectionStateListener { private static final Logger LOG = LoggerFactory.getLogger(CuratorMultiMasterLeaderElection.class); private final String candidateName; private final List<ElectionWatcher> watchers = new ArrayList<ElectionWatcher>(); private final int desiredActiveNodes; private AtomicBoolean activeNode = new AtomicBoolean(false); private UuidGenerator uuidGenerator = new JavaUuidGenerator(); private InterProcessSemaphoreV2 leaderSelector; private CuratorFramework client; private Lease lease; public CuratorMultiMasterLeaderElection(String uri, int desiredActiveNodes) { this.candidateName = createCandidateName(); this.desiredActiveNodes = desiredActiveNodes; String connectionString = uri.substring(1 + uri.indexOf(':')).split("/")[0]; String protocol = uri.substring(0, uri.indexOf(':')); String path = uri.replace(protocol + ":" + connectionString, ""); client = CuratorFrameworkFactory.newClient(connectionString, new ExponentialBackoffRetry(1000, 3)); client.getConnectionStateListenable().addListener(this); leaderSelector = new InterProcessSemaphoreV2(client, path, this.desiredActiveNodes); client.start(); } // stolen from org/apache/camel/processor/CamelInternalProcessor public static boolean isCamelStopping(CamelContext context) { if (context instanceof StatefulService) { StatefulService ss = (StatefulService) context; return ss.isStopping() || ss.isStopped(); } return false; } public void shutdownClients() { try { leaderSelector.returnLease(lease); } finally { client.close(); } } /* * Blocking method */ public void requestResource() { LOG.info("Requested to become active from {}", candidateName); try { lease = leaderSelector.acquire(); } catch (Exception e) { throw new RuntimeException("Unable to obtain access to become a leader node."); } LOG.info("{} is now active", candidateName); activeNode.set(true); notifyElectionWatchers(); } public boolean isMaster() { return activeNode.get(); } private String createCandidateName() { StringBuilder builder = new StringBuilder(); try { /* UUID would be enough, also using hostname for human readability */ builder.append(InetAddress.getLocalHost().getCanonicalHostName()); } catch (UnknownHostException ex) { LOG.warn("Failed to get the local hostname.", ex); builder.append("unknown-host"); } builder.append("-").append(uuidGenerator.generateUuid()); return builder.toString(); } public String getCandidateName() { return candidateName; } private void notifyElectionWatchers() { for (ElectionWatcher watcher : watchers) { try { watcher.electionResultChanged(); } catch (Exception e) { LOG.warn("Election watcher " + watcher + " of type " + watcher.getClass() + " threw an exception.", e); } } } public boolean addElectionWatcher(ElectionWatcher e) { return watchers.add(e); } @Override public void stateChanged(CuratorFramework curatorFramework, ConnectionState connectionState) { switch (connectionState) { case SUSPENDED: case LOST: LOG.info("Received {} state from connection. Giving up lock.", connectionState); try { leaderSelector.returnLease(lease); } finally { this.activeNode.set(false); notifyElectionWatchers(); } break; default: LOG.info("Connection state changed: {}", connectionState); requestResource(); } } }