Skip to content
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ private[kafka010] case class CachedKafkaConsumer private(
c
}

case class AvailableOffsetRange(earliest: Long, latest: Long)

/**
* Return the available offset range of the current partition. It's a pair of the earliest offset
* and the latest offset.
*/
def getAvailableOffsetRange(): AvailableOffsetRange = {
consumer.seekToBeginning(Set(topicPartition).asJava)
val earliestOffset = consumer.position(topicPartition)
consumer.seekToEnd(Set(topicPartition).asJava)
val latestOffset = consumer.position(topicPartition)
AvailableOffsetRange(earliestOffset, latestOffset)
}

/**
* Get the record for the given offset if available. Otherwise it will either throw error
* (if failOnDataLoss = true), or return the next available offset within [offset, untilOffset),
Expand Down Expand Up @@ -107,9 +121,9 @@ private[kafka010] case class CachedKafkaConsumer private(
* `UNKNOWN_OFFSET`.
*/
private def getEarliestAvailableOffsetBetween(offset: Long, untilOffset: Long): Long = {
val (earliestOffset, latestOffset) = getAvailableOffsetRange()
logWarning(s"Some data may be lost. Recovering from the earliest offset: $earliestOffset")
if (offset >= latestOffset || earliestOffset >= untilOffset) {
val range = getAvailableOffsetRange()
logWarning(s"Some data may be lost. Recovering from the earliest offset: ${range.earliest}")
if (offset >= range.latest || range.earliest >= untilOffset) {
// [offset, untilOffset) and [earliestOffset, latestOffset) have no overlap,
// either
// --------------------------------------------------------
Expand All @@ -124,13 +138,13 @@ private[kafka010] case class CachedKafkaConsumer private(
// offset untilOffset earliestOffset latestOffset
val warningMessage =
s"""
|The current available offset range is [$earliestOffset, $latestOffset).
|The current available offset range is [${range.earliest}, ${range.latest}).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: offset range is $range

| Offset ${offset} is out of range, and records in [$offset, $untilOffset) will be
| skipped ${additionalMessage(failOnDataLoss = false)}
""".stripMargin
logWarning(warningMessage)
UNKNOWN_OFFSET
} else if (offset >= earliestOffset) {
} else if (offset >= range.earliest) {
// -----------------------------------------------------------------------------
// ^ ^ ^ ^
// | | | |
Expand All @@ -149,12 +163,12 @@ private[kafka010] case class CachedKafkaConsumer private(
// offset earliestOffset min(untilOffset,latestOffset) max(untilOffset, latestOffset)
val warningMessage =
s"""
|The current available offset range is [$earliestOffset, $latestOffset).
| Offset ${offset} is out of range, and records in [$offset, $earliestOffset) will be
|The current available offset range is [${range.earliest}, ${range.latest}).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same as above.

| Offset ${offset} is out of range, and records in [$offset, ${range.earliest}) will be
| skipped ${additionalMessage(failOnDataLoss = false)}
""".stripMargin
logWarning(warningMessage)
earliestOffset
range.earliest
}
}

Expand Down Expand Up @@ -183,8 +197,8 @@ private[kafka010] case class CachedKafkaConsumer private(
// - `offset` is out of range so that Kafka returns nothing. Just throw
// `OffsetOutOfRangeException` to let the caller handle it.
// - Cannot fetch any data before timeout. TimeoutException will be thrown.
val (earliestOffset, latestOffset) = getAvailableOffsetRange()
if (offset < earliestOffset || offset >= latestOffset) {
val range = getAvailableOffsetRange()
if (offset < range.earliest || offset >= range.latest) {
throw new OffsetOutOfRangeException(
Map(topicPartition -> java.lang.Long.valueOf(offset)).asJava)
} else {
Expand Down Expand Up @@ -284,18 +298,6 @@ private[kafka010] case class CachedKafkaConsumer private(
logDebug(s"Polled $groupId ${p.partitions()} ${r.size}")
fetchedData = r.iterator
}

/**
* Return the available offset range of the current partition. It's a pair of the earliest offset
* and the latest offset.
*/
private def getAvailableOffsetRange(): (Long, Long) = {
consumer.seekToBeginning(Set(topicPartition).asJava)
val earliestOffset = consumer.position(topicPartition)
consumer.seekToEnd(Set(topicPartition).asJava)
val latestOffset = consumer.position(topicPartition)
(earliestOffset, latestOffset)
}
}

private[kafka010] object CachedKafkaConsumer extends Logging {
Expand Down Expand Up @@ -334,14 +336,15 @@ private[kafka010] object CachedKafkaConsumer extends Logging {
def getOrCreate(
topic: String,
partition: Int,
kafkaParams: ju.Map[String, Object]): CachedKafkaConsumer = synchronized {
kafkaParams: ju.Map[String, Object],
reuseExistingIfPresent: Boolean): CachedKafkaConsumer = synchronized {
val groupId = kafkaParams.get(ConsumerConfig.GROUP_ID_CONFIG).asInstanceOf[String]
val topicPartition = new TopicPartition(topic, partition)
val key = CacheKey(groupId, topicPartition)

// If this is reattempt at running the task, then invalidate cache and start with
// a new consumer
if (TaskContext.get != null && TaskContext.get.attemptNumber > 1) {
if (!reuseExistingIfPresent || TaskContext.get != null && TaskContext.get.attemptNumber > 1) {
val removedConsumer = cache.remove(key)
if (removedConsumer != null) {
removedConsumer.close()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.spark.sql.kafka010

import java.{util => ju}

import scala.collection.JavaConverters._

import org.apache.kafka.clients.consumer.{Consumer, KafkaConsumer}
import org.apache.kafka.clients.consumer.internals.NoOpConsumerRebalanceListener
import org.apache.kafka.common.TopicPartition

/**
* Subscribe allows you to subscribe to a fixed collection of topics.
* SubscribePattern allows you to use a regex to specify topics of interest.
* Note that unlike the 0.8 integration, * using Subscribe or SubscribePattern
* should respond to adding partitions during a running stream.
* Finally, Assign allows you to specify a fixed collection of partitions.
* All three strategies have overloaded constructors that allow you to specify
* the starting offset for a particular partition.
*/
sealed trait ConsumerStrategy {
def createConsumer(kafkaParams: ju.Map[String, Object]): Consumer[Array[Byte], Array[Byte]]
}

/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: short docs can be put in a single line /** blah blah */

* Specify a fixed collection of partitions.
*/
case class AssignStrategy(partitions: Array[TopicPartition]) extends ConsumerStrategy {
override def createConsumer(
kafkaParams: ju.Map[String, Object]): Consumer[Array[Byte], Array[Byte]] = {
val consumer = new KafkaConsumer[Array[Byte], Array[Byte]](kafkaParams)
consumer.assign(ju.Arrays.asList(partitions: _*))
consumer
}

override def toString: String = s"Assign[${partitions.mkString(", ")}]"
}

/**
* Subscribe to a fixed collection of topics.
*/
case class SubscribeStrategy(topics: Seq[String]) extends ConsumerStrategy {
override def createConsumer(
kafkaParams: ju.Map[String, Object]): Consumer[Array[Byte], Array[Byte]] = {
val consumer = new KafkaConsumer[Array[Byte], Array[Byte]](kafkaParams)
consumer.subscribe(topics.asJava)
consumer
}

override def toString: String = s"Subscribe[${topics.mkString(", ")}]"
}

/**
* Use a regex to specify topics of interest.
*/
case class SubscribePatternStrategy(topicPattern: String) extends ConsumerStrategy {
override def createConsumer(
kafkaParams: ju.Map[String, Object]): Consumer[Array[Byte], Array[Byte]] = {
val consumer = new KafkaConsumer[Array[Byte], Array[Byte]](kafkaParams)
consumer.subscribe(
ju.regex.Pattern.compile(topicPattern),
new NoOpConsumerRebalanceListener())
consumer
}

override def toString: String = s"SubscribePattern[$topicPattern]"
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,31 @@ package org.apache.spark.sql.kafka010

import org.apache.kafka.common.TopicPartition

/*
* Values that can be specified for config startingOffsets
/**
* Values that can be specified to configure starting,
* ending, and specific offsets.
*/
private[kafka010] sealed trait StartingOffsets
private[kafka010] sealed trait KafkaOffsets

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I think, I feel that its weird to name this generic "KafkaOffsets". Let's brainstorm on this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about KafkaRangeLimit or KafkaOffsetRangeLimit

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with KafkaOffsetRangeLimit


private[kafka010] case object EarliestOffsets extends StartingOffsets
/**
* Bind to the earliest offsets in Kafka

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: better docs. this is object, not a method. say what the object represents. "Bind to earliest offsets..." is like docs for a method

*/
private[kafka010] case object EarliestOffsets extends KafkaOffsets

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs.


private[kafka010] case object LatestOffsets extends StartingOffsets
/**
* Bind to the latest offsets in Kafka
*/
private[kafka010] case object LatestOffsets extends KafkaOffsets

/**
* Bind to the specific offsets. A offset == -1 binds to the latest
* offset, and offset == -2 binds to the earliest offset.
*/
private[kafka010] case class SpecificOffsets(
partitionOffsets: Map[TopicPartition, Long]) extends StartingOffsets
partitionOffsets: Map[TopicPartition, Long]) extends KafkaOffsets

private[kafka010] object KafkaOffsets {
// Used to denote unbounded offset positions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Used to represent unresolved offset limits as longs
"unbounded" sounds like its infinite, or something.

val LATEST = -1L
val EARLIEST = -2L
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.spark.sql.kafka010

import java.{util => ju}

import org.apache.kafka.common.TopicPartition

import org.apache.spark.internal.Logging
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{Row, SQLContext}
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.util.DateTimeUtils
import org.apache.spark.sql.sources.{BaseRelation, TableScan}
import org.apache.spark.sql.types.StructType
import org.apache.spark.unsafe.types.UTF8String


private[kafka010] class KafkaRelation(
override val sqlContext: SQLContext,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incorrect indents

kafkaReader: KafkaTopicPartitionOffsetReader,
executorKafkaParams: ju.Map[String, Object],
sourceOptions: Map[String, String],
failOnDataLoss: Boolean,
startingOffsets: KafkaOffsets,
endingOffsets: KafkaOffsets)
extends BaseRelation with TableScan with Logging {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incorrect indent

assert(startingOffsets != LatestOffsets,
"Starting offset not allowed to be set to latest offsets.")
assert(endingOffsets != EarliestOffsets,
"Ending offset not allowed to be set to earliest offsets.")

private val pollTimeoutMs = sourceOptions.getOrElse(
"kafkaConsumer.pollTimeoutMs",
sqlContext.sparkContext.conf.getTimeAsMs("spark.network.timeout", "120s").toString
).toLong

override def schema: StructType = KafkaTopicPartitionOffsetReader.kafkaSchema

override def buildScan(): RDD[Row] = {
// Leverage the KafkaReader to obtain the relevant partition offsets
val fromPartitionOffsets = getPartitionOffsets(startingOffsets)
val untilPartitionOffsets = getPartitionOffsets(endingOffsets)
// Obtain topicPartitions in both from and until partition offset, ignoring
// topic partitions that were added and/or deleted between the two above calls.
if (fromPartitionOffsets.keySet != untilPartitionOffsets.keySet) {
implicit val topicOrdering: Ordering[TopicPartition] = Ordering.by(t => t.topic())
val fromTopics = fromPartitionOffsets.keySet.toList.sorted.mkString(",")
val untilTopics = untilPartitionOffsets.keySet.toList.sorted.mkString(",")
throw new IllegalStateException("different topic partitions " +
s"for starting offsets topics[${fromTopics}] and " +
s"ending offsets topics[${untilTopics}]")
}

// Calculate offset ranges
val offsetRanges = untilPartitionOffsets.keySet.map { tp =>
val fromOffset = fromPartitionOffsets.get(tp).getOrElse {
// This should not happen since topicPartitions contains all partitions not in
// fromPartitionOffsets
throw new IllegalStateException(s"$tp doesn't have a from offset")
}
val untilOffset = untilPartitionOffsets(tp)
KafkaSourceRDDOffsetRange(tp, fromOffset, untilOffset, None)
}.toArray

logInfo("GetBatch generating RDD of offset range: " +
offsetRanges.sortBy(_.topicPartition.toString).mkString(", "))

// Create an RDD that reads from Kafka and get the (key, value) pair as byte arrays.
val rdd = new KafkaSourceRDD(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found df.union(df) will just union the same RDD which breaks the group id assumption. The same CachedKafkaConsumer will be used by two different tasks. For batch queries, caching consumers is not necessary. Could you add a flag to KafkaSourceRDD to not use the cached consumer? It's better to also write a test to cover this case. In addition, this test should one use one partition in order to launch two tasks from different RDDs at the same time: TestSparkSession uses local[2], so it can only run two tasks at the same time.

sqlContext.sparkContext, executorKafkaParams, offsetRanges,
pollTimeoutMs, failOnDataLoss, reuseKafkaConsumer = false).map { cr =>
InternalRow(
cr.key,
cr.value,
UTF8String.fromString(cr.topic),
cr.partition,
cr.offset,
DateTimeUtils.fromJavaTimestamp(new java.sql.Timestamp(cr.timestamp)),
cr.timestampType.id)
}
sqlContext.internalCreateDataFrame(rdd, schema).rdd
}

private def getPartitionOffsets(kafkaOffsets: KafkaOffsets): Map[TopicPartition, Long] = {
def validateTopicPartitions(partitions: Set[TopicPartition],
partitionOffsets: Map[TopicPartition, Long]): Map[TopicPartition, Long] = {
assert(partitions == partitionOffsets.keySet,
"If startingOffsets contains specific offsets, you must specify all TopicPartitions.\n" +
"Use -1 for latest, -2 for earliest, if you don't care.\n" +
s"Specified: ${partitionOffsets.keySet} Assigned: ${partitions}")
logDebug(s"Partitions assigned to consumer: $partitions. Seeking to $partitionOffsets")
partitionOffsets
}
val partitions = kafkaReader.fetchTopicPartitions()
// Obtain TopicPartition offsets with late binding support
kafkaOffsets match {
case EarliestOffsets => partitions.map {
case tp => tp -> KafkaOffsets.EARLIEST
}.toMap
case LatestOffsets => partitions.map {
case tp => tp -> KafkaOffsets.LATEST
}.toMap
case SpecificOffsets(partitionOffsets) =>
validateTopicPartitions(partitions, partitionOffsets)
}
}
}
Loading