Skip to content

Commit 0a838c1

Browse files
committed
Fixed bugs
1 parent 9e771b0 commit 0a838c1

2 files changed

Lines changed: 110 additions & 49 deletions

File tree

external/kafka-0-10-sql/src/main/scala/org/apache/spark/sql/kafka010/KafkaDataConsumer.scala

Lines changed: 83 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ private[kafka010] case class InternalKafkaConsumer(
8282

8383
private val groupId = kafkaParams.get(ConsumerConfig.GROUP_ID_CONFIG).asInstanceOf[String]
8484

85-
private var consumer = createConsumer
85+
@volatile private var consumer = createConsumer
8686

8787
/** indicates whether this consumer is in use or not */
8888
@volatile var inuse = true
@@ -91,8 +91,9 @@ private[kafka010] case class InternalKafkaConsumer(
9191
@volatile var markedForClose = false
9292

9393
/** Iterator to the already fetch data */
94-
private var fetchedData = ju.Collections.emptyIterator[ConsumerRecord[Array[Byte], Array[Byte]]]
95-
private var nextOffsetInFetchedData = UNKNOWN_OFFSET
94+
@volatile private var fetchedData =
95+
ju.Collections.emptyIterator[ConsumerRecord[Array[Byte], Array[Byte]]]
96+
@volatile private var nextOffsetInFetchedData = UNKNOWN_OFFSET
9697

9798
/** Create a KafkaConsumer to fetch records for `topicPartition` */
9899
private def createConsumer: KafkaConsumer[Array[Byte], Array[Byte]] = {
@@ -358,31 +359,44 @@ private[kafka010] object KafkaDataConsumer extends Logging {
358359

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

361-
private class CachedKafkaDataConsumer(val internalConsumer: InternalKafkaConsumer)
362+
private case class CachedKafkaDataConsumer(internalConsumer: InternalKafkaConsumer)
362363
extends KafkaDataConsumer {
363-
override def release(): Unit = {
364-
releaseKafkaConsumer(internalConsumer.topicPartition, internalConsumer.kafkaParams)
365-
}
364+
override def release(): Unit = { releaseConsumer(internalConsumer) }
366365
}
367366

368-
private class NonCachedKafkaDataConsumer(val internalConsumer: InternalKafkaConsumer)
367+
private case class NonCachedKafkaDataConsumer(internalConsumer: InternalKafkaConsumer)
369368
extends KafkaDataConsumer {
370-
override def release(): Unit = {
371-
internalConsumer.close()
372-
}
369+
override def release(): Unit = { internalConsumer.close() }
373370
}
374371

375372
private case class CacheKey(groupId: String, topicPartition: TopicPartition) {
376373
def this(topicPartition: TopicPartition, kafkaParams: ju.Map[String, Object]) =
377374
this(kafkaParams.get(ConsumerConfig.GROUP_ID_CONFIG).asInstanceOf[String], topicPartition)
378375
}
379376

377+
// This cache has the following important properties.
378+
// - We make a best-effort attempt to maintain the max size of the cache as configured capacity.
379+
// The capacity is not guaranteed to be maintained, especially when there are more active
380+
// tasks simultaneously using consumers than the capacity.
381+
// - A cached consumer will not be cleared while it is in use. This is an invariant that
382+
// we maintain.
380383
private lazy val cache = {
381384
val conf = SparkEnv.get.conf
382385
val capacity = conf.getInt("spark.sql.kafkaConsumerCache.capacity", 64)
383386
new ju.LinkedHashMap[CacheKey, InternalKafkaConsumer](capacity, 0.75f, true) {
384387
override def removeEldestEntry(
385388
entry: ju.Map.Entry[CacheKey, InternalKafkaConsumer]): Boolean = {
389+
390+
// Try to remove the least-used entry if its currently not in use. This maintains the
391+
// invariant mentioned in the docs above.
392+
//
393+
// If you cannot remove it, then the cache will keep growing. In the worst case,
394+
// the cache will grow to the max number of concurrent tasks that can run in the executor,
395+
// (that is, number of tasks slots) after which it will never reduce. This is unlikely to
396+
// be a serious problem because an executor with more than 64 (default) tasks slots is
397+
// likely running on a beefy machine that can handle a large number of simultaneously
398+
// active consumers.
399+
386400
if (entry.getValue.inuse == false && this.size > capacity) {
387401
logWarning(
388402
s"KafkaConsumer cache hitting max capacity of $capacity, " +
@@ -401,30 +415,40 @@ private[kafka010] object KafkaDataConsumer extends Logging {
401415
}
402416
}
403417

404-
private def releaseKafkaConsumer(
405-
topicPartition: TopicPartition,
406-
kafkaParams: ju.Map[String, Object]): Unit = {
407-
val key = new CacheKey(topicPartition, kafkaParams)
408-
418+
private def releaseConsumer(intConsumer: InternalKafkaConsumer): Unit = {
409419
synchronized {
410-
val consumer = cache.get(key)
411-
if (consumer != null) {
412-
if (consumer.markedForClose) {
413-
consumer.close()
420+
421+
// If it has been marked for close, then do it any way
422+
if (intConsumer.inuse && intConsumer.markedForClose) intConsumer.close()
423+
intConsumer.inuse = false
424+
425+
// Clear the consumer from the cache if this is indeed the consumer present in the cache
426+
val key = new CacheKey(intConsumer.topicPartition, intConsumer.kafkaParams)
427+
val cachedIntConsumer = cache.get(key)
428+
if (cachedIntConsumer != null) {
429+
if (cachedIntConsumer.eq(intConsumer)) {
430+
// The released consumer is indeed the cached one.
414431
cache.remove(key)
415432
} else {
416-
consumer.inuse = false
433+
// The released consumer is not the cached one. Don't do anything.
434+
// This should not happen as long as we maintain the invariant mentioned above.
435+
logWarning(
436+
s"Cached consumer not the same one as the one being release" +
437+
s"\ncached = $cachedIntConsumer [${System.identityHashCode(cachedIntConsumer)}]" +
438+
s"\nreleased = $intConsumer [${System.identityHashCode(intConsumer)}]")
417439
}
418440
} else {
419-
logWarning(s"Attempting to release consumer that does not exist")
441+
// The released consumer is not in the cache. Don't do anything.
442+
// This should not happen as long as we maintain the invariant mentioned above.
443+
logWarning(s"Attempting to release consumer that is not in the cache")
420444
}
421445
}
422446
}
423447

424-
425448
/**
426449
* Get a cached consumer for groupId, assigned to topic and partition.
427450
* If matching consumer doesn't already exist, will be created using kafkaParams.
451+
* This will make a best effort attempt to
428452
*/
429453
def acquire(
430454
topicPartition: TopicPartition,
@@ -433,26 +457,50 @@ private[kafka010] object KafkaDataConsumer extends Logging {
433457
val key = new CacheKey(topicPartition, kafkaParams)
434458
val existingInternalConsumer = cache.get(key)
435459

436-
lazy val newNonCachedConsumer =
437-
new NonCachedKafkaDataConsumer(new InternalKafkaConsumer(topicPartition, kafkaParams))
460+
lazy val newInternalConsumer = new InternalKafkaConsumer(topicPartition, kafkaParams)
438461

439462
if (TaskContext.get != null && TaskContext.get.attemptNumber >= 1) {
440-
// If this is reattempt at running the task, then invalidate cache and start with
441-
// a new consumer
463+
// If this is reattempt at running the task, then invalidate cached consumer if any and
464+
// start with a new one.
442465
if (existingInternalConsumer != null) {
443-
existingInternalConsumer.markedForClose = true
466+
if (existingInternalConsumer.inuse) {
467+
// Consumer exists in cache and is somehow in use. Don't close it immediately, but
468+
// mark it for being closed when it is released.
469+
existingInternalConsumer.markedForClose = true
470+
NonCachedKafkaDataConsumer(newInternalConsumer)
471+
472+
} else {
473+
// Consumer exists in cache and is not in use, so close it immediately and replace
474+
// it with a new one.
475+
existingInternalConsumer.close()
476+
cache.put(key, newInternalConsumer)
477+
CachedKafkaDataConsumer(newInternalConsumer)
478+
479+
}
480+
} else {
481+
// Consumer is not cached, put the new one in the cache
482+
cache.put(key, newInternalConsumer)
483+
CachedKafkaDataConsumer(newInternalConsumer)
484+
444485
}
445-
newNonCachedConsumer
446486
} else if (!useCache) {
447-
newNonCachedConsumer
487+
// If planner asks to not reuse consumers, then do not use it, return a new consumer
488+
NonCachedKafkaDataConsumer(newInternalConsumer)
489+
448490
} else if (existingInternalConsumer == null) {
449-
newNonCachedConsumer.internalConsumer.inuse = true
450-
cache.put(key, newNonCachedConsumer.internalConsumer)
451-
newNonCachedConsumer
491+
// If consumer is not already cached, then put a new in the cache and return it
492+
newInternalConsumer.inuse = true
493+
cache.put(key, newInternalConsumer)
494+
CachedKafkaDataConsumer(newInternalConsumer)
495+
452496
} else if (existingInternalConsumer.inuse) {
453-
newNonCachedConsumer
497+
// If consumer is already cached but is currently in use, then return a new consumer
498+
NonCachedKafkaDataConsumer(newInternalConsumer)
499+
454500
} else {
455-
new CachedKafkaDataConsumer(existingInternalConsumer)
501+
// If consumer is already cached and is currently not in use, then return that consumer
502+
CachedKafkaDataConsumer(existingInternalConsumer)
503+
456504
}
457505
}
458506
}

external/kafka-0-10-sql/src/test/scala/org/apache/spark/sql/kafka010/KafkaDataConsumerSuite.scala

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import org.apache.kafka.common.TopicPartition
2929
import org.apache.kafka.common.serialization.ByteArrayDeserializer
3030
import org.scalatest.PrivateMethodTester
3131

32+
import org.apache.spark.{TaskContext, TaskContextImpl}
3233
import org.apache.spark.sql.test.SharedSQLContext
3334
import org.apache.spark.util.ThreadUtils
3435

@@ -59,10 +60,11 @@ class KafkaDataConsumerSuite extends SharedSQLContext with PrivateMethodTester {
5960
assert(e.getCause === cause)
6061
}
6162

62-
test("concurrent use of KafkaDataConsumer") {
63+
test("SPARK-23623: concurrent use of KafkaDataConsumer") {
6364
val topic = "topic" + Random.nextInt()
65+
val data = (1 to 1000).map(_.toString)
6466
testUtils.createTopic(topic, 1)
65-
testUtils.sendMessages(topic, (1 to 1000).map(_.toString).toArray)
67+
testUtils.sendMessages(topic, data.toArray)
6668
val topicPartition = new TopicPartition(topic, 0)
6769

6870
import ConsumerConfig._
@@ -78,18 +80,25 @@ class KafkaDataConsumerSuite extends SharedSQLContext with PrivateMethodTester {
7880
val numThreads = 50
7981
val numConsumerUsages = 500
8082

81-
val threadpool = Executors.newFixedThreadPool(numThreads)
82-
8383
@volatile var error: Throwable = null
8484

8585
def consume(i: Int): Unit = {
86+
val useCache = Random.nextBoolean
87+
val taskContext = if (Random.nextBoolean) {
88+
new TaskContextImpl(0, 0, 0, 0, attemptNumber = Random.nextInt(2), null, null, null)
89+
} else {
90+
null
91+
}
92+
TaskContext.setTaskContext(taskContext)
8693
val consumer = KafkaDataConsumer.acquire(
87-
topicPartition, kafkaParams.asJava, useCache = true)
94+
topicPartition, kafkaParams.asJava, useCache)
8895
try {
8996
val range = consumer.getAvailableOffsetRange()
90-
for (offset <- range.earliest until range.latest) {
91-
consumer.get(offset, Long.MaxValue, 10000, failOnDataLoss = false)
97+
val rcvd = range.earliest until range.latest map { offset =>
98+
val bytes = consumer.get(offset, Long.MaxValue, 10000, failOnDataLoss = false).value()
99+
new String(bytes)
92100
}
101+
assert(rcvd == data)
93102
} catch {
94103
case e: Throwable =>
95104
error = e
@@ -99,13 +108,17 @@ class KafkaDataConsumerSuite extends SharedSQLContext with PrivateMethodTester {
99108
}
100109
}
101110

102-
// Sub
103-
val futures = (1 to numConsumerUsages).map { i =>
104-
threadpool.submit(new Runnable {
105-
override def run(): Unit = { consume(i) }
106-
})
111+
val threadpool = Executors.newFixedThreadPool(numThreads)
112+
try {
113+
val futures = (1 to numConsumerUsages).map { i =>
114+
threadpool.submit(new Runnable {
115+
override def run(): Unit = { consume(i) }
116+
})
117+
}
118+
futures.foreach(_.get(1, TimeUnit.MINUTES))
119+
assert(error == null)
120+
} finally {
121+
threadpool.shutdown()
107122
}
108-
futures.foreach(_.get(1, TimeUnit.MINUTES))
109-
assert(error == null)
110123
}
111124
}

0 commit comments

Comments
 (0)