// ================================================================================================= // 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.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; 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.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. * * @author William Farner */ public class SingletonService { private static final Logger LOG = Logger.getLogger(SingletonService.class.getName()); private static final String LEADER_ELECT_NODE_PREFIX = "singleton_candidate_"; 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)), new CandidateImpl(new Group(zkClient, acl, servicePath, LEADER_ELECT_NODE_PREFIX))); } @VisibleForTesting 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 Current status of the candidate. * @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 Status status, final LeadershipListener listener) throws Group.WatchException, Group.JoinException, InterruptedException { Preconditions.checkNotNull(listener); candidate.offerLeadership(new Leader() { private EndpointStatus endpointStatus = null; @Override public void onElected(ExceptionalCommand<JoinException> abdicate) { try { endpointStatus = serverSet.join(endpoint, additionalEndpoints, status); listener.onLeading(endpointStatus); } catch (Group.JoinException e) { LOG.log(Level.SEVERE, "Failed to join group.", e); } catch (InterruptedException e) { LOG.log(Level.SEVERE, "Interrupted while joining group.", e); Thread.currentThread().interrupt(); } } @Override public void onDefeated() { listener.onDefeated(endpointStatus); } }); } public static interface LeadershipListener { public void onLeading(EndpointStatus status); public void onDefeated(@Nullable EndpointStatus status); } }