-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-2808][Streaming][Kafka] update kafka to 0.8.2 #4537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 20 commits
e768164
d9dc2bc
2e67c66
6953429
77de6c2
407382e
ed02d2c
1d10751
c70ee43
9edab4c
af6f3ec
61b3464
3824ce3
2b92d3f
2712649
115aeee
4c4557f
1d896e2
30d991d
d4267e9
1770abc
e6dfaf6
803aa2c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,9 +20,10 @@ package org.apache.spark.streaming.kafka | |
| import scala.util.control.NonFatal | ||
| import scala.util.Random | ||
| import scala.collection.mutable.ArrayBuffer | ||
| import scala.collection.JavaConverters._ | ||
| import java.util.Properties | ||
| import kafka.api._ | ||
| import kafka.common.{ErrorMapping, OffsetMetadataAndError, TopicAndPartition} | ||
| import kafka.common.{ErrorMapping, OffsetAndMetadata, OffsetMetadataAndError, TopicAndPartition} | ||
| import kafka.consumer.{ConsumerConfig, SimpleConsumer} | ||
| import org.apache.spark.SparkException | ||
|
|
||
|
|
@@ -37,6 +38,11 @@ private[spark] | |
| class KafkaCluster(val kafkaParams: Map[String, String]) extends Serializable { | ||
| import KafkaCluster.{Err, LeaderOffset, SimpleConsumerConfig} | ||
|
|
||
| /** Constructor that takes a Java map */ | ||
| def this(kafkaParams: java.util.Map[String, String]) { | ||
| this(kafkaParams.asScala.toMap) | ||
| } | ||
|
|
||
| // ConsumerConfig isn't serializable | ||
| @transient private var _config: SimpleConsumerConfig = null | ||
|
|
||
|
|
@@ -220,12 +226,22 @@ class KafkaCluster(val kafkaParams: Map[String, String]) extends Serializable { | |
| // https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-OffsetCommit/FetchAPI | ||
| // scalastyle:on | ||
|
|
||
| // this 0 here indicates api version, in this case the original ZK backed api. | ||
| def defaultConsumerApiVersion: Short = 0 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be private? Do other parts of the spark/streaming call this function? |
||
|
|
||
| /** Requires Kafka >= 0.8.1.1 */ | ||
| def getConsumerOffsets( | ||
| groupId: String, | ||
| topicAndPartitions: Set[TopicAndPartition] | ||
| ): Either[Err, Map[TopicAndPartition, Long]] = | ||
| getConsumerOffsets(groupId, topicAndPartitions, defaultConsumerApiVersion) | ||
|
|
||
| def getConsumerOffsets( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: These two can be merged with default arguments.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code was originally using default arguments. That's what was causing the MiMa binary compatibility errors
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that is frigging weird. This is in an internal class and should not be throwing MIMA compatibility errors. :/ |
||
| groupId: String, | ||
| topicAndPartitions: Set[TopicAndPartition], | ||
| consumerApiVersion: Short | ||
| ): Either[Err, Map[TopicAndPartition, Long]] = { | ||
| getConsumerOffsetMetadata(groupId, topicAndPartitions).right.map { r => | ||
| getConsumerOffsetMetadata(groupId, topicAndPartitions, consumerApiVersion).right.map { r => | ||
| r.map { kv => | ||
| kv._1 -> kv._2.offset | ||
| } | ||
|
|
@@ -236,9 +252,16 @@ class KafkaCluster(val kafkaParams: Map[String, String]) extends Serializable { | |
| def getConsumerOffsetMetadata( | ||
| groupId: String, | ||
| topicAndPartitions: Set[TopicAndPartition] | ||
| ): Either[Err, Map[TopicAndPartition, OffsetMetadataAndError]] = | ||
| getConsumerOffsetMetadata(groupId, topicAndPartitions, defaultConsumerApiVersion) | ||
|
|
||
| def getConsumerOffsetMetadata( | ||
| groupId: String, | ||
| topicAndPartitions: Set[TopicAndPartition], | ||
| consumerApiVersion: Short | ||
| ): Either[Err, Map[TopicAndPartition, OffsetMetadataAndError]] = { | ||
| var result = Map[TopicAndPartition, OffsetMetadataAndError]() | ||
| val req = OffsetFetchRequest(groupId, topicAndPartitions.toSeq) | ||
| val req = OffsetFetchRequest(groupId, topicAndPartitions.toSeq, consumerApiVersion) | ||
| val errs = new Err | ||
| withBrokers(Random.shuffle(config.seedBrokers), errs) { consumer => | ||
| val resp = consumer.fetchOffsets(req) | ||
|
|
@@ -266,24 +289,39 @@ class KafkaCluster(val kafkaParams: Map[String, String]) extends Serializable { | |
| def setConsumerOffsets( | ||
| groupId: String, | ||
| offsets: Map[TopicAndPartition, Long] | ||
| ): Either[Err, Map[TopicAndPartition, Short]] = | ||
| setConsumerOffsets(groupId, offsets, defaultConsumerApiVersion) | ||
|
|
||
| def setConsumerOffsets( | ||
| groupId: String, | ||
| offsets: Map[TopicAndPartition, Long], | ||
| consumerApiVersion: Short | ||
| ): Either[Err, Map[TopicAndPartition, Short]] = { | ||
| setConsumerOffsetMetadata(groupId, offsets.map { kv => | ||
| kv._1 -> OffsetMetadataAndError(kv._2) | ||
| }) | ||
| val meta = offsets.map { kv => | ||
| kv._1 -> OffsetAndMetadata(kv._2) | ||
| } | ||
| setConsumerOffsetMetadata(groupId, meta, consumerApiVersion) | ||
| } | ||
|
|
||
| /** Requires Kafka >= 0.8.1.1 */ | ||
| def setConsumerOffsetMetadata( | ||
| groupId: String, | ||
| metadata: Map[TopicAndPartition, OffsetMetadataAndError] | ||
| metadata: Map[TopicAndPartition, OffsetAndMetadata] | ||
| ): Either[Err, Map[TopicAndPartition, Short]] = | ||
| setConsumerOffsetMetadata(groupId, metadata, defaultConsumerApiVersion) | ||
|
|
||
| def setConsumerOffsetMetadata( | ||
| groupId: String, | ||
| metadata: Map[TopicAndPartition, OffsetAndMetadata], | ||
| consumerApiVersion: Short | ||
| ): Either[Err, Map[TopicAndPartition, Short]] = { | ||
| var result = Map[TopicAndPartition, Short]() | ||
| val req = OffsetCommitRequest(groupId, metadata) | ||
| val req = OffsetCommitRequest(groupId, metadata, consumerApiVersion) | ||
| val errs = new Err | ||
| val topicAndPartitions = metadata.keySet | ||
| withBrokers(Random.shuffle(config.seedBrokers), errs) { consumer => | ||
| val resp = consumer.commitOffsets(req) | ||
| val respMap = resp.requestInfo | ||
| val respMap = resp.commitStatus | ||
| val needed = topicAndPartitions.diff(result.keySet) | ||
| needed.foreach { tp: TopicAndPartition => | ||
| respMap.get(tp).foreach { err: Short => | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,10 +29,12 @@ import scala.language.postfixOps | |
| import scala.util.control.NonFatal | ||
|
|
||
| import kafka.admin.AdminUtils | ||
| import kafka.api.Request | ||
| import kafka.common.TopicAndPartition | ||
| import kafka.producer.{KeyedMessage, Producer, ProducerConfig} | ||
| import kafka.serializer.StringEncoder | ||
| import kafka.server.{KafkaConfig, KafkaServer} | ||
| import kafka.utils.ZKStringSerializer | ||
| import kafka.utils.{ZKStringSerializer, ZkUtils} | ||
| import org.apache.zookeeper.server.{NIOServerCnxnFactory, ZooKeeperServer} | ||
| import org.I0Itec.zkclient.ZkClient | ||
|
|
||
|
|
@@ -227,10 +229,34 @@ private class KafkaTestUtils extends Logging { | |
| tryAgain(1) | ||
| } | ||
|
|
||
| /** wait until the leader offset for the given topic / partition equals the specified offset */ | ||
| def waitUntilLeaderOffset( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this function do? Either make the name more meaningful (like |
||
| kc: KafkaCluster, | ||
| topic: String, | ||
| partition: Int, | ||
| offset: Long): Unit = { | ||
| eventually(Time(10000), Time(100)) { | ||
| val tp = TopicAndPartition(topic, partition) | ||
| val llo = kc.getLatestLeaderOffsets(Set(tp)).right.get.apply(tp).offset | ||
| assert( | ||
| llo == offset, | ||
| s"$topic $partition $offset not reached after timeout") | ||
| } | ||
| } | ||
|
|
||
| private def waitUntilMetadataIsPropagated(topic: String, partition: Int): Unit = { | ||
| eventually(Time(10000), Time(100)) { | ||
| assert( | ||
| server.apis.metadataCache.containsTopicAndPartition(topic, partition), | ||
| server.apis.metadataCache.getPartitionInfo(topic, partition) match { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you put this big piece of code in a separate
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And also explain in a comment what this code does? For example, I am not sure what is |
||
| case Some(partitionState) => | ||
| val leaderAndIsr = partitionState.leaderIsrAndControllerEpoch.leaderAndIsr | ||
| ZkUtils.getLeaderForPartition(zkClient, topic, partition).isDefined && | ||
| Request.isValidBrokerId(leaderAndIsr.leader) && | ||
| leaderAndIsr.isr.size >= 1 | ||
|
|
||
| case _ => | ||
| false | ||
| }, | ||
| s"Partition [$topic, $partition] metadata not propagated after timeout" | ||
| ) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,14 +53,16 @@ class KafkaRDDSuite extends FunSuite with BeforeAndAfterAll { | |
| } | ||
|
|
||
| test("basic usage") { | ||
| val topic = "topicbasic" | ||
| val topic = s"topicbasic-${Random.nextInt}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was this change necessary? |
||
| kafkaTestUtils.createTopic(topic) | ||
| val messages = Set("the", "quick", "brown", "fox") | ||
| kafkaTestUtils.sendMessages(topic, messages.toArray) | ||
|
|
||
|
|
||
| val kafkaParams = Map("metadata.broker.list" -> kafkaTestUtils.brokerAddress, | ||
| "group.id" -> s"test-consumer-${Random.nextInt(10000)}") | ||
| "group.id" -> s"test-consumer-${Random.nextInt}") | ||
|
|
||
| val kc = new KafkaCluster(kafkaParams) | ||
| kafkaTestUtils.waitUntilLeaderOffset(kc, topic, 0, messages.size) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What was this, and why was this necessary?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Those were just ruling out the possibility of jenkins failures being related to stale duplicate topic or consumer state. There's nothing magical about the number 10000 as a random range for the name, so I removed it.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense for the topic name change. This question was about the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, thats' what got the tests passing on Jenkins |
||
|
|
||
| val offsetRanges = Array(OffsetRange(topic, 0, 0, messages.size)) | ||
|
|
||
|
|
@@ -73,34 +75,46 @@ class KafkaRDDSuite extends FunSuite with BeforeAndAfterAll { | |
|
|
||
| test("iterator boundary conditions") { | ||
| // the idea is to find e.g. off-by-one errors between what kafka has available and the rdd | ||
| val topic = "topic1" | ||
| val topic = s"topicboundary-${Random.nextInt}" | ||
| val sent = Map("a" -> 5, "b" -> 3, "c" -> 10) | ||
| kafkaTestUtils.createTopic(topic) | ||
|
|
||
| val kafkaParams = Map("metadata.broker.list" -> kafkaTestUtils.brokerAddress, | ||
| "group.id" -> s"test-consumer-${Random.nextInt(10000)}") | ||
| "group.id" -> s"test-consumer-${Random.nextInt}") | ||
|
|
||
| val kc = new KafkaCluster(kafkaParams) | ||
|
|
||
| // this is the "lots of messages" case | ||
| kafkaTestUtils.sendMessages(topic, sent) | ||
| val sentCount = sent.values.sum | ||
| kafkaTestUtils.waitUntilLeaderOffset(kc, topic, 0, sentCount) | ||
|
|
||
| // rdd defined from leaders after sending messages, should get the number sent | ||
| val rdd = getRdd(kc, Set(topic)) | ||
|
|
||
| assert(rdd.isDefined) | ||
| assert(rdd.get.count === sent.values.sum, "didn't get all sent messages") | ||
|
|
||
| val ranges = rdd.get.asInstanceOf[HasOffsetRanges] | ||
| .offsetRanges.map(o => TopicAndPartition(o.topic, o.partition) -> o.untilOffset).toMap | ||
| val ranges = rdd.get.asInstanceOf[HasOffsetRanges].offsetRanges | ||
| val rangeCount = ranges.map(o => o.untilOffset - o.fromOffset).sum | ||
|
|
||
| kc.setConsumerOffsets(kafkaParams("group.id"), ranges) | ||
| assert(rangeCount === sentCount, "offset range didn't include all sent messages") | ||
| assert(rdd.get.count === sentCount, "didn't get all sent messages") | ||
|
|
||
| val rangesMap = ranges.map(o => TopicAndPartition(o.topic, o.partition) -> o.untilOffset).toMap | ||
|
|
||
| kc.setConsumerOffsets(kafkaParams("group.id"), rangesMap).fold( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this for? Its not obvious, could you add a comment? |
||
| err => throw new Exception(err.mkString("\n")), | ||
| _ => () | ||
| ) | ||
|
|
||
| // this is the "0 messages" case | ||
| val rdd2 = getRdd(kc, Set(topic)) | ||
| // shouldn't get anything, since message is sent after rdd was defined | ||
| val sentOnlyOne = Map("d" -> 1) | ||
|
|
||
| kafkaTestUtils.sendMessages(topic, sentOnlyOne) | ||
| kafkaTestUtils.waitUntilLeaderOffset(kc, topic, 0, sentCount + 1) | ||
|
|
||
| assert(rdd2.isDefined) | ||
| assert(rdd2.get.count === 0, "got messages when there shouldn't be any") | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -606,7 +606,6 @@ def _validateRddResult(self, sendData, rdd): | |
| result = {} | ||
| for i in rdd.map(lambda x: x[1]).collect(): | ||
| result[i] = result.get(i, 0) + 1 | ||
|
|
||
| self.assertEqual(sendData, result) | ||
|
|
||
| def test_kafka_stream(self): | ||
|
|
@@ -616,6 +615,7 @@ def test_kafka_stream(self): | |
|
|
||
| self._kafkaTestUtils.createTopic(topic) | ||
| self._kafkaTestUtils.sendMessages(topic, sendData) | ||
| time.sleep(5) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the |
||
|
|
||
| stream = KafkaUtils.createStream(self.ssc, self._kafkaTestUtils.zkAddress(), | ||
| "test-streaming-consumer", {topic: 1}, | ||
|
|
@@ -659,7 +659,7 @@ def test_kafka_rdd(self): | |
|
|
||
| self._kafkaTestUtils.createTopic(topic) | ||
| self._kafkaTestUtils.sendMessages(topic, sendData) | ||
|
|
||
| time.sleep(5) | ||
| rdd = KafkaUtils.createRDD(self.sc, kafkaParams, offsetRanges) | ||
| self._validateRddResult(sendData, rdd) | ||
|
|
||
|
|
@@ -675,7 +675,7 @@ def test_kafka_rdd_with_leaders(self): | |
|
|
||
| self._kafkaTestUtils.createTopic(topic) | ||
| self._kafkaTestUtils.sendMessages(topic, sendData) | ||
|
|
||
| time.sleep(5) | ||
| rdd = KafkaUtils.createRDD(self.sc, kafkaParams, offsetRanges, leaders) | ||
| self._validateRddResult(sendData, rdd) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this used anywhere? If it is not, then why was this added?