// =================================================================================================
// 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.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import org.apache.zookeeper.KeeperException;
import com.twitter.common.base.Command;
import com.twitter.common.base.ExceptionalCommand;
import com.twitter.common.zookeeper.Group.GroupChangeListener;
import com.twitter.common.zookeeper.Group.JoinException;
import com.twitter.common.zookeeper.Group.Membership;
import com.twitter.common.zookeeper.Group.WatchException;
import com.twitter.common.zookeeper.ZooKeeperClient.ZooKeeperConnectionException;
/**
* Implements leader election for small groups of candidates. This implementation is subject to the
* <a href="http://hadoop.apache.org/zookeeper/docs/r3.2.1/recipes.html#sc_leaderElection">
* herd effect</a> for a given group and should only be used for small (~10 member) candidate pools.
*
* @author John Sirois
*/
public class CandidateImpl implements Candidate {
private static final Logger LOG = Logger.getLogger(CandidateImpl.class.getName());
private final Group group;
private final Function<Iterable<String>, String> judge;
private final Supplier<byte[]> dataSupplier;
static final Supplier<byte[]> IP_ADDRESS_DATA_SUPPLIER = new Supplier<byte[]>() {
@Override
public byte[] get() {
try {
return InetAddress.getLocalHost().getAddress();
} catch (UnknownHostException e) {
LOG.log(Level.WARNING, "Failed to determine local address!", e);
return new byte[0];
}
}
};
public static final Function<Iterable<String>, String> MOST_RECENT_JUDGE =
new Function<Iterable<String>, String>() {
@Override public String apply(Iterable<String> candidates) {
return Ordering.natural().min(candidates);
}
};
/**
* Equivalent to {@link #CandidateImpl(Group, com.google.common.base.Function, Supplier)} using a judge that
* always picks the lowest numbered candidate ephemeral node - by proxy the oldest or 1st
* candidate and a default supplier that populates the data in the underlying znode with the byte representation
* of the ip address according to {@link java.net.InetAddress#getLocalHost()}.
*/
public CandidateImpl(Group group) {
this(group, MOST_RECENT_JUDGE, IP_ADDRESS_DATA_SUPPLIER);
}
/**
* Creates a candidate that can be used to offer leadership for the given {@code group}. The
* {@code judge} is used to pick the current leader from all group members whenever the group
* membership changes. To form a well-behaved election group with one leader, all candidates
* should use the same judge. The dataSupplier is the source of the data that will be stored
* in the leader-znode and which is available to all participants via the getLeaderData method.
*/
public CandidateImpl(Group group, Function<Iterable<String>, String> judge, Supplier<byte[]> dataSupplier) {
this.group = Preconditions.checkNotNull(group);
this.judge = Preconditions.checkNotNull(judge);
this.dataSupplier = Preconditions.checkNotNull(dataSupplier);
}
@Override
public byte[] getLeaderData()
throws ZooKeeperConnectionException, KeeperException, InterruptedException {
String leaderId = getLeader(group.getMemberIds());
if (leaderId == null) {
return null;
}
byte[] data = group.getMemberData(leaderId);
return data == null ? new byte[0] : data;
}
@Override
public Supplier<Boolean> offerLeadership(final Leader leader)
throws JoinException, WatchException, InterruptedException {
final Membership membership = group.join(dataSupplier, new Command() {
@Override public void execute() {
leader.onDefeated();
}
});
final AtomicBoolean elected = new AtomicBoolean(false);
final AtomicBoolean abdicated = new AtomicBoolean(false);
group.watch(new GroupChangeListener() {
@Override public void onGroupChange(Iterable<String> memberIds) {
boolean noCandidates = Iterables.isEmpty(memberIds);
String memberId = membership.getMemberId();
if (noCandidates) {
LOG.warning("All candidates have temporarily left the group: " + group);
} else if (!Iterables.contains(memberIds, memberId)) {
LOG.severe(String.format(
"Current member ID %s is not a candidate for leader, current voting: %s",
memberId, memberIds));
} else {
boolean electedLeader = memberId.equals(getLeader(memberIds));
boolean previouslyElected = elected.getAndSet(electedLeader);
if (!previouslyElected && electedLeader) {
LOG.info(String.format("Candidate %s is now leader of group: %s",
membership.getMemberPath(), memberIds));
leader.onElected(new ExceptionalCommand<JoinException>() {
@Override public void execute() throws JoinException {
membership.cancel();
abdicated.set(true);
}
});
} else if (!electedLeader) {
if (previouslyElected) {
leader.onDefeated();
}
LOG.info(String.format(
"Candidate %s waiting for the next leader election, current voting: %s",
membership.getMemberPath(), memberIds));
}
}
}
});
return new Supplier<Boolean>() {
@Override public Boolean get() {
return !abdicated.get() && elected.get();
}
};
}
private String getLeader(Iterable<String> memberIds) {
return judge.apply(memberIds);
}
}