All Downloads are FREE. Search and download functionalities are using the official Maven repository.

co.cask.cdap.kafka.flow.KafkaBrokerCache Maven / Gradle / Ivy

/*
 * Copyright © 2014 Cask Data, Inc.
 *
 * Licensed 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 co.cask.cdap.kafka.flow;

import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.TreeMultimap;
import com.google.common.util.concurrent.AbstractIdleService;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.twill.common.Threads;
import org.apache.twill.zookeeper.NodeChildren;
import org.apache.twill.zookeeper.NodeData;
import org.apache.twill.zookeeper.ZKClient;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.SortedMap;

/**
 * Provides information about Kafka brokers by watching for changes in ZooKeeper. It is specific for Kafka 0.7
 * ZooKeeper layout (https://cwiki.apache.org/confluence/display/KAFKA/Writing+a+Driver+for+Kafka)
 * 

* In brief, Kafka 0.7 has the following ZK node structure: * *

 * /brokers
 *   /ids
 *     /[broker1_id]
 *     /[broker2_id]
 *   /topics
 *     /[topic_1]
 *       /[broker1_id]
 *     /[topic_2]
 *       /[broker1_id]
 * 
* * * * * * * * * * * * * * * * * * * * *
RoleZooKeeper PathTypeData Description
ID Registry
/brokers/ids/[0..N]
EphemeralString in the format of "creator:host:port" of the broker.
Topic Registry
/brokers/topics/[topic]/[0..N]
EphemeralNumber of partitions that topic has on that Broker.
*/ final class KafkaBrokerCache extends AbstractIdleService { private static final Logger LOG = LoggerFactory.getLogger(KafkaBrokerCache.class); private static final String BROKERS_PATH = "/brokers"; private final ZKClient zkClient; private final Map brokers; // topicBrokers is from topic->partition size->brokerId private final Map>> topicBrokers; private final Runnable invokeGetBrokers = new Runnable() { @Override public void run() { getBrokers(); } }; private final Runnable invokeGetTopics = new Runnable() { @Override public void run() { getTopics(); } }; KafkaBrokerCache(ZKClient zkClient) { this.zkClient = zkClient; this.brokers = Maps.newConcurrentMap(); this.topicBrokers = Maps.newConcurrentMap(); } @Override protected void startUp() throws Exception { getBrokers(); getTopics(); } @Override protected void shutDown() throws Exception { // No-op } public int getPartitionSize(String topic) { SortedMap> partitionBrokers = topicBrokers.get(topic); if (partitionBrokers == null || partitionBrokers.isEmpty()) { return 1; } return partitionBrokers.lastKey(); } /** * Returns a list of {@link KafkaBroker} that contains the given topic and partition. The list * is already sorted by the broker id in ascending order. */ public List getBrokers(String topic, int partition) { SortedMap> partitionBrokers = topicBrokers.get(topic); // If there is no broker for the topic partition, return empty list if (partitionBrokers == null || partitionBrokers.isEmpty() || partition >= partitionBrokers.lastKey()) { return ImmutableList.of(); } List result = new ArrayList<>(); for (String brokerId : Iterables.concat(partitionBrokers.tailMap(partition + 1).values())) { result.add(brokers.get(brokerId)); } Collections.sort(result); return result; } /** * Returns a random broker address or {@code null} if none are available. */ public KafkaBroker getRandomBroker() { Collection brokers = this.brokers.values(); if (brokers.isEmpty()) { return null; } return Iterables.get(brokers, new Random().nextInt(brokers.size()), null); } /** * Gets brokerIds (async) and starts watching for changes. */ private void getBrokers() { final String idsPath = BROKERS_PATH + "/ids"; Futures.addCallback(zkClient.getChildren(idsPath, new Watcher() { @Override public void process(WatchedEvent event) { if (isRunning()) { getBrokers(); } } }), new ExistsOnFailureFutureCallback(idsPath, invokeGetBrokers) { @Override public void onSuccess(NodeChildren result) { Set children = ImmutableSet.copyOf(result.getChildren()); for (String child : children) { getBrokenData(idsPath + "/" + child, child); } // Remove all removed brokers removeDiff(children, brokers); } }); } /** * Gets all topic nodes (async). Also leave a node watch for changes of topics. */ private void getTopics() { final String topicsPath = BROKERS_PATH + "/topics"; Futures.addCallback(zkClient.getChildren(topicsPath, new Watcher() { @Override public void process(WatchedEvent event) { if (isRunning()) { getTopics(); } } }), new ExistsOnFailureFutureCallback(topicsPath, invokeGetTopics) { @Override public void onSuccess(NodeChildren result) { Set children = ImmutableSet.copyOf(result.getChildren()); // Process new topic. For existing topic, changes in broker list under it is being watched already. for (String topic : Sets.difference(children, topicBrokers.keySet())) { getTopicBrokers(topicsPath + "/" + topic, topic); } // Remove old topics removeDiff(children, topicBrokers); } }); } /** * Gets the broker node data. The broker data has the form {@code creator:host:port}. * * @param path ZK path to the broker node * @param brokerId The broker Id */ private void getBrokenData(String path, final String brokerId) { Futures.addCallback(zkClient.getData(path), new FutureCallback() { @Override public void onSuccess(NodeData result) { // result.getData() shouldn't be null, as it's data written by Kafka when the node was created. String data = new String(result.getData(), Charsets.UTF_8); // We are only interested in host and port Iterator splits = Iterables.skip(Splitter.on(':').split(data), 1).iterator(); String host = splits.next(); int port = Integer.parseInt(splits.next()); brokers.put(brokerId, new KafkaBroker(brokerId, host, port)); } @Override public void onFailure(Throwable t) { // No-op, the watch on the parent node will handle it. } }); } /** * Gets all broker associated with the given topic. It also starts watching for changes in broker list. * * @param path ZK path to the topic node * @param topic the topic */ private void getTopicBrokers(final String path, final String topic) { Futures.addCallback(zkClient.getChildren(path, new Watcher() { @Override public void process(WatchedEvent event) { if (!isRunning()) { return; } // Other event type changes are either could be ignored or handled by parent watcher if (event.getType() == Event.EventType.NodeChildrenChanged) { getTopicBrokers(path, topic); } } }), new FutureCallback() { @Override public void onSuccess(NodeChildren result) { List children = result.getChildren(); final List> futures = new ArrayList<>(children.size()); // Fetch data (number of partitions) from each broken node and transform it to BrokerPartition for (final String brokerId : children) { futures.add(Futures.transform(zkClient.getData(path + "/" + brokerId), new Function() { @Override public BrokerPartition apply(NodeData input) { return new BrokerPartition(brokerId, Integer.parseInt(new String(input.getData(), Charsets.UTF_8))); } })); } // When all fetching is done, build the partition size->broker map for this topic Futures.addCallback(Futures.successfulAsList(futures), new FutureCallback>() { @Override public void onSuccess(List result) { TreeMultimap partitionBrokers = TreeMultimap.create(); for (BrokerPartition brokerPartition : result) { if (brokerPartition == null) { // Ignore if getData() on the broker node failed (hence got null result). continue; } partitionBrokers.put(brokerPartition.getPartitionSize(), brokerPartition.getBrokerId()); } topicBrokers.put(topic, partitionBrokers.asMap()); } @Override public void onFailure(Throwable t) { // This never happens, which is the contract of successfulAsList. } }, Threads.SAME_THREAD_EXECUTOR); } @Override public void onFailure(Throwable t) { // No-op. Failure would be handled by parent watcher already (e.g. node not exists -> children change in parent) } }); } /** * Removes all Map entries that are not present in the provided key Set. */ private void removeDiff(Set keys, Map map) { for (K key : Sets.difference(map.keySet(), keys)) { map.remove(key); } } /** * A {@link FutureCallback} for ZK operations so that if an operation failed due * to {@link KeeperException.Code#NONODE} error, it will watch for the existence of the given node and perform * a given action when the node become available. Upon node removal, the node will again be watched for creation * and will trigger the given action again once it is created. * * @param result type of the Future it is listening on. */ private abstract class ExistsOnFailureFutureCallback implements FutureCallback { private final String path; private final Runnable action; protected ExistsOnFailureFutureCallback(String path, Runnable action) { this.path = path; this.action = action; } @Override public final void onFailure(Throwable t) { if (!isNotExists(t)) { LOG.error("Operation failed for path {}", path, t); return; } // If failed due to not exist error, watch for creation of the node. waitExists(path); } private boolean isNotExists(Throwable t) { return ((t instanceof KeeperException) && ((KeeperException) t).code() == KeeperException.Code.NONODE); } private void waitExists(final String path) { LOG.debug("Path {} not exists. Watch for creation.", path); // If the node doesn't exists, use the "exists" call to watch for node creation. Futures.addCallback(zkClient.exists(path, new Watcher() { @Override public void process(WatchedEvent event) { if (!isRunning()) { return; } if (event.getType() == Event.EventType.NodeCreated) { action.run(); } else if (event.getType() == Event.EventType.NodeDeleted) { // If the node is deleted, keep watching it. waitExists(path); } } }), new FutureCallback() { @Override public void onSuccess(Stat result) { // If path exists on the exists call, execution the action. if (result != null) { action.run(); } } @Override public void onFailure(Throwable t) { LOG.error("Failed to get existence of path {}", path, t); } }); } } /** * A helper POJO to hold brokerId and the number of partitions (of a given topic) for that broker. */ private static final class BrokerPartition { private final String brokerId; private final int partitionSize; private BrokerPartition(String brokerId, int partitionSize) { this.brokerId = brokerId; this.partitionSize = partitionSize; } public String getBrokerId() { return brokerId; } public int getPartitionSize() { return partitionSize; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy