@@ -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 " \n cached = $cachedIntConsumer [ ${System .identityHashCode(cachedIntConsumer)}] " +
438+ s " \n released = $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}
0 commit comments