// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.zookeeper; import java.net.InetSocketAddress; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.Watcher.Event.EventType; import org.apache.zookeeper.Watcher.Event.KeeperState; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.data.ACL; import com.twitter.common.base.ExceptionalCommand; import com.twitter.common.zookeeper.Candidate.Leader; import com.twitter.common.zookeeper.Group.JoinException; import com.twitter.common.zookeeper.ServerSet.EndpointStatus; import com.twitter.common.zookeeper.ServerSet.UpdateException; import com.twitter.thrift.Status; /** * A service that uses master election to only allow a single instance of the server to join * the {@link ServerSet} at a time. */ public class SingletonService { private static final Logger LOG = Logger.getLogger(SingletonService.class.getName()); @VisibleForTesting static final String LEADER_ELECT_NODE_PREFIX = "singleton_candidate_"; /** * Creates a candidate that can be combined with an existing server set to form a singleton * service using {@link #SingletonService(ServerSet, Candidate)}. * * @param zkClient The ZooKeeper client to use. * @param servicePath The path where service nodes live. * @param acl The acl to apply to newly created candidate nodes and serverset nodes. * @return A candidate that can be housed with a standard server set under a single zk path. */ public static Candidate createSingletonCandidate( ZooKeeperClient zkClient, String servicePath, Iterable<ACL> acl) { return new CandidateImpl(new Group(zkClient, acl, servicePath, LEADER_ELECT_NODE_PREFIX)); } private final ServerSet serverSet; private final Candidate candidate; /** * Equivalent to {@link #SingletonService(ZooKeeperClient, String, Iterable)} with a default * wide open {@code acl} ({@link ZooDefs.Ids#OPEN_ACL_UNSAFE}). */ public SingletonService(ZooKeeperClient zkClient, String servicePath) { this(zkClient, servicePath, ZooDefs.Ids.OPEN_ACL_UNSAFE); } /** * Creates a new singleton service, identified by {@code servicePath}. All nodes related to the * service (for both leader election and service registration) will live under the path and each * node will be created with the supplied {@code acl}. Internally, two ZooKeeper {@code Group}s * are used to manage a singleton service - one for leader election, and another for the * {@code ServerSet} where the leader's endpoints are registered. Leadership election should * guarantee that at most one instance will ever exist in the ServerSet at once. * * @param zkClient The ZooKeeper client to use. * @param servicePath The path where service nodes live. * @param acl The acl to apply to newly created candidate nodes and serverset nodes. */ public SingletonService(ZooKeeperClient zkClient, String servicePath, Iterable<ACL> acl) { this( new ServerSetImpl(zkClient, new Group(zkClient, acl, servicePath)), createSingletonCandidate(zkClient, servicePath, acl)); } /** * Creates a new singleton service that uses the supplied candidate to vie for leadership and then * advertises itself in the given server set once elected. * * @param serverSet The server set to advertise in on election. * @param candidate The candidacy to use to vie for election. */ public SingletonService(ServerSet serverSet, Candidate candidate) { this.serverSet = Preconditions.checkNotNull(serverSet); this.candidate = Preconditions.checkNotNull(candidate); } /** * Attempts to lead the singleton service. * * @param endpoint The primary endpoint to register as a leader candidate in the service. * @param additionalEndpoints Additional endpoints that are available on the host. * @param status deprecated, will be ignored entirely * @param listener Handler to call when the candidate is elected or defeated. * @throws Group.WatchException If there was a problem watching the ZooKeeper group. * @throws Group.JoinException If there was a problem joining the ZooKeeper group. * @throws InterruptedException If the thread watching/joining the group was interrupted. * @deprecated The status field is deprecated. Please use * {@link #lead(InetSocketAddress, Map, LeadershipListener)} */ @Deprecated public void lead(final InetSocketAddress endpoint, final Map<String, InetSocketAddress> additionalEndpoints, final Status status, final LeadershipListener listener) throws Group.WatchException, Group.JoinException, InterruptedException { if (status != Status.ALIVE) { LOG.severe("******************************************************************************"); LOG.severe("WARNING: MUTABLE STATUS FIELDS ARE NO LONGER SUPPORTED."); LOG.severe("JOINING WITH STATUS ALIVE EVEN THOUGH YOU SPECIFIED " + status); LOG.severe("******************************************************************************"); } else { LOG.warning("******************************************************************************"); LOG.warning("WARNING: MUTABLE STATUS FIELDS ARE NO LONGER SUPPORTED."); LOG.warning("Please use SingletonService.lead(InetSocketAddress, Map, LeadershipListener)"); LOG.warning("******************************************************************************"); } lead(endpoint, additionalEndpoints, listener); } /** * Attempts to lead the singleton service. * * @param endpoint The primary endpoint to register as a leader candidate in the service. * @param additionalEndpoints Additional endpoints that are available on the host. * @param listener Handler to call when the candidate is elected or defeated. * @throws Group.WatchException If there was a problem watching the ZooKeeper group. * @throws Group.JoinException If there was a problem joining the ZooKeeper group. * @throws InterruptedException If the thread watching/joining the group was interrupted. */ public void lead(final InetSocketAddress endpoint, final Map<String, InetSocketAddress> additionalEndpoints, final LeadershipListener listener) throws Group.WatchException, Group.JoinException, InterruptedException { Preconditions.checkNotNull(listener); candidate.offerLeadership(new Leader() { private EndpointStatus endpointStatus = null; @Override public void onElected(final ExceptionalCommand<JoinException> abdicate) { listener.onLeading(new LeaderControl() { EndpointStatus endpointStatus = null; final AtomicBoolean left = new AtomicBoolean(false); // Methods are synchronized to prevent simultaneous invocations. @Override public synchronized void advertise() throws JoinException, InterruptedException { Preconditions.checkState(!left.get(), "Cannot advertise after leaving."); Preconditions.checkState(endpointStatus == null, "Cannot advertise more than once."); endpointStatus = serverSet.join(endpoint, additionalEndpoints); } @Override public synchronized void leave() throws UpdateException, JoinException { Preconditions.checkState(left.compareAndSet(false, true), "Cannot leave more than once."); if (endpointStatus != null) { endpointStatus.leave(); } abdicate.execute(); } }); } @Override public void onDefeated() { listener.onDefeated(endpointStatus); } }); } /** * A listener to be notified of changes in the leadership status. * Implementers should be careful to avoid blocking operations in these callbacks. */ public interface LeadershipListener { /** * Notifies the listener that is is current leader. * * @param control A controller handle to advertise and/or leave advertised presence. */ public void onLeading(LeaderControl control); /** * Notifies the listener that it is no longer leader. The leader should take this opportunity * to remove its advertisement gracefully. * * @param status A handle on the endpoint status for the advertised leader. */ public void onDefeated(@Nullable EndpointStatus status); } /** * A leadership listener that decorates another listener by automatically defeating a * leader that has dropped its connection to ZooKeeper. * Note that the decision to use this over session-based mutual exclusion should not be taken * lightly. Any momentary connection loss due to a flaky network or a ZooKeeper server process * exit will cause a leader to abort. */ public static class DefeatOnDisconnectLeader implements LeadershipListener { private final LeadershipListener wrapped; private Optional<LeaderControl> maybeControl = Optional.absent(); /** * Creates a new leadership listener that will delegate calls to the wrapped listener, and * invoke {@link #onDefeated(EndpointStatus)} if a ZooKeeper disconnect is observed while * leading. * * @param zkClient The ZooKeeper client to watch for disconnect events. * @param wrapped The leadership listener to wrap. */ public DefeatOnDisconnectLeader(ZooKeeperClient zkClient, LeadershipListener wrapped) { this.wrapped = Preconditions.checkNotNull(wrapped); zkClient.register(new Watcher() { @Override public void process(WatchedEvent event) { if ((event.getType() == EventType.None) && (event.getState() == KeeperState.Disconnected)) { disconnected(); } } }); } private synchronized void disconnected() { if (maybeControl.isPresent()) { LOG.warning("Disconnected from ZooKeeper while leading, committing suicide."); try { wrapped.onDefeated(null); maybeControl.get().leave(); } catch (UpdateException e) { LOG.log(Level.WARNING, "Failed to leave singleton service: " + e, e); } catch (JoinException e) { LOG.log(Level.WARNING, "Failed to leave singleton service: " + e, e); } finally { setControl(null); } } else { LOG.info("Disconnected from ZooKeeper, but that's fine because I'm not the leader."); } } private synchronized void setControl(@Nullable LeaderControl control) { this.maybeControl = Optional.fromNullable(control); } @Override public void onLeading(final LeaderControl control) { setControl(control); wrapped.onLeading(new LeaderControl() { @Override public void advertise() throws JoinException, InterruptedException { control.advertise(); } @Override public void leave() throws UpdateException, JoinException { setControl(null); control.leave(); } }); } @Override public void onDefeated(@Nullable EndpointStatus status) { setControl(null); wrapped.onDefeated(status); } } /** * A controller for the state of the leader. This will be provided to the leader upon election, * which allows the leader to decide when to advertise in the underlying {@link ServerSet} and * terminate leadership at will. */ public interface LeaderControl { /** * Advertises the leader's server presence to clients. * * @throws JoinException If there was an error advertising. * @throws InterruptedException If interrupted while advertising. */ void advertise() throws JoinException, InterruptedException; /** * Leaves candidacy for leadership, removing advertised server presence if applicable. * * @throws UpdateException If the leader's status could not be updated. * @throws JoinException If there was an error abdicating from leader election. */ void leave() throws UpdateException, JoinException; } }