package com.linkedin.camus.etl.kafka.common; import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import kafka.api.PartitionFetchInfo; import kafka.common.TopicAndPartition; import kafka.javaapi.FetchRequest; import kafka.javaapi.FetchResponse; import kafka.javaapi.PartitionMetadata; import kafka.javaapi.TopicMetadata; import kafka.javaapi.TopicMetadataRequest; import kafka.javaapi.TopicMetadataResponse; import kafka.javaapi.consumer.SimpleConsumer; import kafka.javaapi.message.ByteBufferMessageSet; import kafka.message.Message; import kafka.message.MessageAndOffset; import org.apache.hadoop.mapreduce.TaskAttemptContext; import org.apache.log4j.Logger; import com.linkedin.camus.etl.kafka.CamusJob; import com.linkedin.camus.etl.kafka.mapred.EtlInputFormat; /** * Poorly named class that handles kafka pull events within each * KafkaRecordReader. * * @author Richard Park */ public class KafkaReader { // index of context private static Logger log = Logger.getLogger(KafkaReader.class); private EtlRequest kafkaRequest = null; private SimpleConsumer simpleConsumer = null; private long beginOffset; private long currentOffset; private long lastOffset; private long currentCount; private TaskAttemptContext context; private Iterator<MessageAndOffset> messageIter = null; private long totalFetchTime = 0; private long lastFetchTime = 0; private int fetchBufferSize; /** * Construct using the json representation of the kafka request */ public KafkaReader(EtlInputFormat inputFormat, TaskAttemptContext context, EtlRequest request, int clientTimeout, int fetchBufferSize) throws Exception { this.fetchBufferSize = fetchBufferSize; this.context = context; log.info("bufferSize=" + fetchBufferSize); log.info("timeout=" + clientTimeout); // Create the kafka request from the json kafkaRequest = request; beginOffset = request.getOffset(); currentOffset = request.getOffset(); lastOffset = request.getLastOffset(); currentCount = 0; totalFetchTime = 0; // read data from queue URI uri = kafkaRequest.getURI(); simpleConsumer = inputFormat.createSimpleConsumer(context, uri.getHost(), uri.getPort()); log.info("Connected to leader " + uri + " beginning reading at offset " + beginOffset + " latest offset=" + lastOffset); fetch(); } public boolean hasNext() throws IOException { if (currentOffset >= lastOffset) { return false; } if (messageIter != null && messageIter.hasNext()) { return true; } else { return fetch(); } } /** * Fetches the next Kafka message and stuffs the results into the key and * value * * @param etlKey * @return true if there exists more events * @throws IOException */ public KafkaMessage getNext(EtlKey etlKey) throws IOException { if (hasNext()) { MessageAndOffset msgAndOffset = messageIter.next(); Message message = msgAndOffset.message(); byte[] payload = getBytes(message.payload()); byte[] key = getBytes(message.key()); if (payload == null) { log.warn("Received message with null message.payload(): " + msgAndOffset); } etlKey.clear(); etlKey.set(kafkaRequest.getTopic(), kafkaRequest.getLeaderId(), kafkaRequest.getPartition(), currentOffset, msgAndOffset.offset() + 1, message.checksum()); etlKey.setMessageSize(msgAndOffset.message().size()); currentOffset = msgAndOffset.offset() + 1; // increase offset currentCount++; // increase count return new KafkaMessage(payload, key, kafkaRequest.getTopic(), kafkaRequest.getPartition(), msgAndOffset.offset(), message.checksum()); } else { return null; } } private byte[] getBytes(ByteBuffer buf) { byte[] bytes = null; if (buf != null) { int size = buf.remaining(); bytes = new byte[size]; buf.get(bytes, buf.position(), size); } return bytes; } /** * Creates a fetch request. * * @return false if there's no more fetches * @throws IOException */ public boolean fetch() throws IOException { if (currentOffset >= lastOffset) { return false; } long tempTime = System.currentTimeMillis(); TopicAndPartition topicAndPartition = new TopicAndPartition(kafkaRequest.getTopic(), kafkaRequest.getPartition()); log.debug("\nAsking for offset : " + (currentOffset)); PartitionFetchInfo partitionFetchInfo = new PartitionFetchInfo(currentOffset, fetchBufferSize); HashMap<TopicAndPartition, PartitionFetchInfo> fetchInfo = new HashMap<TopicAndPartition, PartitionFetchInfo>(); fetchInfo.put(topicAndPartition, partitionFetchInfo); FetchRequest fetchRequest = new FetchRequest(CamusJob.getKafkaFetchRequestCorrelationId(context), CamusJob.getKafkaClientName(context), CamusJob.getKafkaFetchRequestMaxWait(context), CamusJob.getKafkaFetchRequestMinBytes(context), fetchInfo); FetchResponse fetchResponse = null; try { fetchResponse = simpleConsumer.fetch(fetchRequest); if (fetchResponse.hasError()) { String message = "Error Code generated : " + fetchResponse.errorCode(kafkaRequest.getTopic(), kafkaRequest.getPartition()) + "\n"; throw new RuntimeException(message); } return processFetchResponse(fetchResponse, tempTime); } catch (Exception e) { log.info("Exception generated during fetch for topic " + kafkaRequest.getTopic() + ": " + e.getMessage() + ". Will refresh topic metadata and retry."); return refreshTopicMetadataAndRetryFetch(fetchRequest, tempTime); } } private boolean refreshTopicMetadataAndRetryFetch(FetchRequest fetchRequest, long tempTime) { try { refreshTopicMetadata(); FetchResponse fetchResponse = simpleConsumer.fetch(fetchRequest); if (fetchResponse.hasError()) { log.warn("Error encountered during fetch request retry from Kafka"); log.warn("Error Code generated : " + fetchResponse.errorCode(kafkaRequest.getTopic(), kafkaRequest.getPartition())); return false; } return processFetchResponse(fetchResponse, tempTime); } catch (Exception e) { log.info("Exception generated during fetch for topic " + kafkaRequest.getTopic() + ". This topic will be skipped."); return false; } } private void refreshTopicMetadata() { TopicMetadataRequest request = new TopicMetadataRequest(Collections.singletonList(kafkaRequest.getTopic())); TopicMetadataResponse response; try { response = simpleConsumer.send(request); } catch (Exception e) { log.error("Exception caught when refreshing metadata for topic " + request.topics().get(0) + ": " + e.getMessage()); return; } TopicMetadata metadata = response.topicsMetadata().get(0); for (PartitionMetadata partitionMetadata : metadata.partitionsMetadata()) { if (partitionMetadata.partitionId() == kafkaRequest.getPartition()) { simpleConsumer = new SimpleConsumer(partitionMetadata.leader().host(), partitionMetadata.leader().port(), CamusJob.getKafkaTimeoutValue(context), CamusJob.getKafkaBufferSize(context), CamusJob.getKafkaClientName(context)); break; } } } private boolean processFetchResponse(FetchResponse fetchResponse, long tempTime) { try { ByteBufferMessageSet messageBuffer = fetchResponse.messageSet(kafkaRequest.getTopic(), kafkaRequest.getPartition()); lastFetchTime = (System.currentTimeMillis() - tempTime); log.debug("Time taken to fetch : " + (lastFetchTime / 1000) + " seconds"); log.debug("The size of the ByteBufferMessageSet returned is : " + messageBuffer.sizeInBytes()); int skipped = 0; totalFetchTime += lastFetchTime; messageIter = messageBuffer.iterator(); //boolean flag = false; Iterator<MessageAndOffset> messageIter2 = messageBuffer.iterator(); MessageAndOffset message = null; while (messageIter2.hasNext()) { message = messageIter2.next(); if (message.offset() < currentOffset) { //flag = true; skipped++; } else { log.debug("Skipped offsets till : " + message.offset()); break; } } log.debug("Number of offsets to be skipped: " + skipped); while (skipped != 0) { MessageAndOffset skippedMessage = messageIter.next(); log.debug("Skipping offset : " + skippedMessage.offset()); skipped--; } if (!messageIter.hasNext()) { System.out.println("No more data left to process. Returning false"); messageIter = null; return false; } return true; } catch (Exception e) { log.info("Exception generated during processing fetchResponse"); return false; } } /** * Closes this context * * @throws IOException */ public void close() throws IOException { if (simpleConsumer != null) { simpleConsumer.close(); } } /** * Returns the total bytes that will be fetched. This is calculated by * taking the diffs of the offsets * * @return */ public long getTotalBytes() { return (lastOffset > beginOffset) ? lastOffset - beginOffset : 0; } /** * Returns the total bytes that have been fetched so far * * @return */ public long getReadBytes() { return currentOffset - beginOffset; } /** * Returns the number of events that have been read r * * @return */ public long getCount() { return currentCount; } /** * Returns the fetch time of the last fetch in ms * * @return */ public long getFetchTime() { return lastFetchTime; } /** * Returns the totalFetchTime in ms * * @return */ public long getTotalFetchTime() { return totalFetchTime; } }