package com.twitter.common.zookeeper.testing.angrybird; import java.io.IOException; import java.text.ParseException; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.List; import javax.annotation.Nullable; import com.google.common.base.Optional; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.net.HostAndPort; import org.apache.commons.lang.StringUtils; import org.apache.zookeeper.KeeperException.NoNodeException; import org.apache.zookeeper.data.Stat; import org.apache.zookeeper.server.ZKDatabase; import com.twitter.common.application.ShutdownRegistry; import com.twitter.common.zookeeper.testing.ZooKeeperTestServer; import com.twitter.common.zookeeper.testing.angrybird.gen.TestEndpoint; /** * ZooKeeper server harness for fault testing. * * You may expire sessions directly by SessionId, by Endpoint (host, port pair) or * by leader/follower in a group of ephemeral/sequential nodes. * * The Endpoint encoding currently assumes that the format of the content in the znode is an * "id@host:port" string. */ public class AngryBirdZooKeeperServer extends ZooKeeperTestServer { private static final Logger LOG = Logger.getLogger(AngryBirdZooKeeperServer.class.getName()); private static final Splitter AT_SPLITTER = Splitter.on('@').trimResults().omitEmptyStrings(); public AngryBirdZooKeeperServer(int port, ShutdownRegistry shutdownRegistry) throws IOException { super(port, shutdownRegistry); } /** * Expires the zookeeper session id. * * @param sessionId The Zookeeper session id * @return the sessionId if the session was successfully closed */ public final Optional<Long> expireSession(Long sessionId) { return closeSession(Optional.of(sessionId)); } /** * Expires the zookeeper session of the given endpoint. * For now, this only supports those endpoints that store their host:port in the znode. * * @param host host address of the endpoint stored in the znode * @param port port of the endpoint stored in the znode * @return the session id of the znode that matches the endpoint if a match is found */ public final Optional<Long> expireEndpoint(String host, int port) { return closeSession(getSessionIdFromHostPair(host, port)); } /** * Expires zookeeper session of leader rooted at a znode. * * @param path the zookeeper path that is used for leader election * @return the session id of the matching candidate if a match is found */ public final Optional<Long> expireLeader(String path) { return closeSession(getLeaderSessionIdFromPath(path)); } /** * Expires zookeeper session of follower rooted at a znode. * * @param path the zookeeper path that is used for leader election * @param nodeId (optional) expire a specific follower node if found * @return the session id of the matching candidate if a match is found */ public final Optional<Long> expireFollower(String path, Optional<Integer> nodeId) { return closeSession(getFollowerSessionIdFromPath(path, nodeId)); } private Optional<Long> closeSession(Optional<Long> sessionId) { if (!sessionId.isPresent()) { LOG.warning("No session found for expiration!"); } else { LOG.info("Closing session: " + sessionId); zooKeeperServer.closeSession(sessionId.get().longValue()); } return sessionId; } /** * Returns the session whose corresponding znode encodes "host:port" * * @param host ip address of the endpoint * @param port endpoint port * @return session id of the corresponding zk session if a match is found. */ private Optional<Long> getSessionIdFromHostPair(String host, int port) { // TODO(vinod): Instead of (host, port) args use the more generic byte[] as args // so that comparison can be made on znodes that are ServerSet ephemerals ZKDatabase zkDb = zooKeeperServer.getZKDatabase(); for (long sessionId : zkDb.getSessions()) { for (String path: zkDb.getEphemerals(sessionId)) { LOG.info("SessionId:" + sessionId + " Path:" + path); try { String data = new String(zkDb.getData(path, new Stat(), null)); LOG.info("Data in znode: " + data); TestEndpoint endpoint = parseEndpoint(data); LOG.info("Extracted endpoint " + endpoint); if (endpoint.getHost().equals(host) && endpoint.getPort() == port) { LOG.info(String.format( "Matching session id %s found for endpoint %s:%s", sessionId, host, port)); return Optional.of(sessionId); } } catch (NoNodeException e) { LOG.severe("Exception getting data for Path:" + path + " : " + e); } catch (ParseException e) { LOG.severe("Exception parsing data: " + e); } catch (NumberFormatException e) { LOG.severe("Exception in url format " + e); } } } return Optional.absent(); } /** * Return the session id of the leader candidate. * See http://zookeeper.apache.org/doc/trunk/recipes.html#sc_leaderElection * * @param zkPath Znode path prefix of the candidates * @return the session id of the corresponding zk session if a match is found. */ private Optional<Long> getLeaderSessionIdFromPath(String zkPath) { ZKDatabase zkDb = zooKeeperServer.getZKDatabase(); Long leaderSessionId = null; Long masterSeq = Long.MAX_VALUE; // Reg-ex pattern for sequence numbers in znode paths. Pattern pattern = Pattern.compile("\\d+$"); // First find the session id of the leading scheduler. for (long sessionId : zkDb.getSessions()) { for (String path: zkDb.getEphemerals(sessionId)) { if (StringUtils.startsWith(path, zkPath)) { try { // Get the sequence number. Matcher matcher = pattern.matcher(path); if (matcher.find()) { LOG.info("Pattern matched path: " + path + " session: " + sessionId); Long seq = Long.parseLong(matcher.group()); if (seq < masterSeq) { masterSeq = seq; leaderSessionId = sessionId; } } } catch (NumberFormatException e) { LOG.severe("Exception formatting sequence number " + e); } } } } if (leaderSessionId != null) { LOG.info(String.format("Found session leader for %s: %s", zkPath, leaderSessionId)); } return Optional.of(leaderSessionId); } /** * Return the session id of a follower candidate * * @param zkPath Znode path prefix of the candidates * @param candidateId (optional) specific candidate id of follower to expire, otherwise random. * @return session id of the corresponding zk session if a match is found */ private Optional<Long> getFollowerSessionIdFromPath(String zkPath, Optional<Integer> nodeId) { Optional<Long> leaderSessionId = getLeaderSessionIdFromPath(zkPath); if (!leaderSessionId.isPresent()) { return leaderSessionId; } ZKDatabase zkDb = zooKeeperServer.getZKDatabase(); for (long sessionId : zkDb.getSessions()) { if (sessionId == leaderSessionId.get()) { continue; } for (String path: zkDb.getEphemerals(sessionId)) { if (StringUtils.startsWith(path, zkPath)) { LOG.info(String.format("Found session follower for %s: %s", zkPath, sessionId)); if (!nodeId.isPresent()) { return Optional.of(sessionId); } else { TestEndpoint endpoint; try { endpoint = parseEndpoint(new String(zkDb.getData(path, new Stat(), null))); if (endpoint.getNodeId() == nodeId.get()) { return Optional.of(sessionId); } } catch (ParseException e) { LOG.severe("Failed to parse endpoint " + path + ": " + e); } catch (NoNodeException e) { LOG.severe("Exception getting data for Path:" + path + " :" + e); } } } } } return Optional.absent(); } private TestEndpoint parseEndpoint(String data) throws ParseException { ImmutableList<String> endpointComponents = ImmutableList.copyOf(AT_SPLITTER.split(data)); if (endpointComponents.size() != 2) { throw new ParseException("Unknown znode data: Expected format id@host:port", 0); } int nodeId = Integer.parseInt(endpointComponents.get(0)); HostAndPort pair; try { pair = HostAndPort.fromString(endpointComponents.get(1)); } catch (IllegalArgumentException e) { throw new ParseException("Failed to parse endpoint data: " + endpointComponents.get(1), data.indexOf('@')); } TestEndpoint endpoint = new TestEndpoint(); endpoint.setNodeId(nodeId); endpoint.setHost(pair.getHostText()); endpoint.setPort(pair.getPort()); return endpoint; } }